Harness Academy
Tài liệu gốc 26 14 phútTiếng Việt + English
docs/en/s23-trajectory-and-rl.mdXem bản gốc
Ngôn ngữ bài đọc
Bản dịch tiếng Việt

Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.

s23: Đào tạo quỹ đạo & RL

s00 > s01 > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12 > s13 > s14 > s15 > s16 > s17 > s18 > s19 > s20 > s21 > s22 > [ s23 ] > s24 > s25 > s26 > s27

s20-s22 cho phép agent học trong runtime -- cập nhật bộ nhớ, tạo kỹ năng, gắn móc. Chương này cho phép agent phát triển ngoại tuyến -- biến quỹ đạo hội thoại thành dữ liệu huấn luyện và sử dụng học tăng cường để huấn luyện thế hệ mô hình tiếp theo.

Quỹ đạo và đường ống RL

Chương này giải quyết vấn đề gì

Quá trình xem xét lý lịch từ s20 và tạo kỹ năng từ s21 thể hiện học tập trong runtime -- kiến thức mà tổng đài viên khám phá trong cuộc trò chuyện sẽ được chuyển tới chính họ trong tương lai thông qua các tệp (MEMORY.md, Skills/).

Nhưng điều này có giới hạn: các tập tin lưu trữ "kiến thức" chứ không phải "khả năng". agent có thể biết rằng "người dùng không thích gợi ý loại" (kiến thức), nhưng khả năng viết mã của nó không được cải thiện chỉ vì bộ nhớ đó.

Để tự cải thiện khả năng, bạn cần đào tạo mô hình.

Đường ống đầy đủ:

Conversation -> trajectory collection -> trajectory compression -> reward scoring -> RL training -> better model

Kịch bản: Từ 1.000 cuộc trò chuyện đến mô hình mạnh mẽ hơn.

1. batch_runner runs the agent on 500 prompts, 2 runs per prompt
   -> 1,000 conversation trajectories, stored as JSONL

2. trajectory_compressor compresses long trajectories to under 15K tokens
   -> Preserves key information at head and tail, replaces middle with summaries

3. Environment scoring: each trajectory receives 0-2 points based on task completion
   -> Complete = 2.0, partial = 1.0, failure = 0.0

4. GRPO training: fine-tune the model using reward signals
   -> 2,500 steps, LoRA rank 32, lr 4e-5

5. The new model performs better on the same prompts

Đây không phải là lý thuyết -- mô hình của agent Hermes được lặp lại chính xác theo cách này.

Đề xuất đọc

Các khái niệm chính

Quỹ đạo là gì

Bản ghi của một cuộc trò chuyện hoàn chỉnh, được định dạng ở định dạng ShareGPT:

[
  {"from": "system", "value": "You are a helpful assistant..."},
  {"from": "human",  "value": "Write me a Python script"},
  {"from": "gpt",    "value": "<think>Let me analyze the requirements first...</think>Sure, I'll write..."},
  {"from": "gpt",    "value": "<tool_call>{\"name\": \"write_file\", ...}</tool_call>"},
  {"from": "tool",   "value": "<tool_response>{\"content\": \"Written 50 chars\"}</tool_response>"},
  {"from": "gpt",    "value": "The file is ready. You can try running it."}
]

Gần giống với các thông báo phiên trong SQLite, nhưng có hai phép biến đổi chính:

  • Lý luận (quá trình suy nghĩ của mô hình) được bao bọc thống nhất trong <think> thẻ
  • Các cuộc gọi công cụ được gói gọn trong <tool_call> / <tool_response> thẻ

batch_runner là gì

Một công cụ để chạy agent với số lượng lớn. Nó nhận một danh sách các lời nhắc (JSONL), khởi chạy một phiên bản agent cho mỗi lời nhắc, đồng thời thu thập quỹ đạo và số liệu thống kê.

Hỗ trợ song song, nối lại điểm kiểm tra và tự động lọc các mẫu chất lượng thấp.

quỹ đạo_compressor là gì

Một máy nén quỹ đạo. Nhiều quỹ đạo hội thoại rất dài (hàng chục nghìn token), khiến việc đào tạo trực tiếp không hiệu quả. Trình nén giữ nguyên nội dung chính ở phần đầu và phần đuôi, thay thế phần giữa bằng bản tóm tắt do LLM tạo và đưa quỹ đạo xuống độ dài mục tiêu (mặc định là 15.250 token).

GRPO là gì

Tối ưu hóa chính sách tương đối của nhóm. Một thuật toán học tăng cường phù hợp hơn với các mô hình ngôn ngữ so với PPO. Ý tưởng cốt lõi: tạo nhiều quỹ đạo cho cùng một lời nhắc, sử dụng tín hiệu khen thưởng để cho mô hình biết "cái nào tốt hơn" và cập nhật mô hình để hướng tới kết quả tốt hơn.

Mô hình tinh thần tối thiểu

Phase 1: Collection
  prompts.jsonl -> batch_runner -> trajectory_samples.jsonl
                                  + tool_stats
                                  + reasoning_stats

Phase 2: Compression
  trajectory_samples.jsonl -> trajectory_compressor -> compressed.jsonl
    (50K tokens)              (preserve head/tail,     (15K tokens)
                               compress middle)

Phase 3: Scoring
  compressed.jsonl -> environment scoring function -> scored.jsonl
    each trajectory + original prompt                (trajectory + reward)

Phase 4: Training
  scored.jsonl -> GRPO trainer -> new model weights
    multiple trajectories per prompt     LoRA adapters
    reinforce high-reward, weaken low-reward

Cấu trúc dữ liệu chính

Bản ghi quỹ đạo (đầu ra batch_runner)

{
  "trajectory": [
    {"from": "system", "value": "..."},
    {"from": "human", "value": "..."},
    {"from": "gpt", "value": "<think>...</think>..."}
  ],
  "tool_stats": {
    "terminal": {"count": 5, "success": 4, "failure": 1},
    "read_file": {"count": 3, "success": 3, "failure": 0}
  },
  "reasoning_stats": {
    "total_assistant_turns": 8,
    "turns_with_reasoning": 6,
    "turns_without_reasoning": 2,
    "has_any_reasoning": true
  },
  "completed": true,
  "api_calls": 12,
  "toolsets_used": ["terminal", "file"]
}

Cấu hình nén

tokenizer_name: "moonshotai/Kimi-K2-Thinking"
target_max_tokens: 15250
summary_target_tokens: 750

# Protection policy: don't compress head or tail
protect_first_system: true
protect_first_human: true
protect_first_gpt: true
protect_last_n_turns: 4

# Summarization model
summarization_model: "google/gemini-3-flash-preview"
summarization_temperature: 0.3

Chức năng khen thưởng

def correctness_reward(prompts, completions, answer):
    """2.0 points if fully correct, 0.0 if wrong."""
    rewards = []
    for completion, expected in zip(completions, answer):
        if expected in completion:
            rewards.append(2.0)
        else:
            rewards.append(0.0)
    return rewards

def format_reward(completions):
    """0.5 points if format is correct (has <think> and <tool_call> tags)."""
    rewards = []
    for c in completions:
        score = 0.0
        if "<think>" in c and "</think>" in c:
            score += 0.25
        if "<tool_call>" in c:
            score += 0.25
        rewards.append(score)
    return rewards

Các hàm phần thưởng có thể kết hợp được -- tính chính xác mang lại điểm cao nhất (2,0), trong khi định dạng đóng vai trò là tín hiệu phụ (0,5). GRPO sử dụng số tiền có trọng số làm phần thưởng cuối cùng.

Thực hiện tối thiểu

Bước 1: Chuyển đổi định dạng quỹ đạo

Chuyển đổi tin nhắn từ run_conversation sang định dạng ShareGPT:

def convert_to_trajectory(messages: list[dict]) -> list[dict]:
    """Convert OpenAI-format messages to ShareGPT trajectory format."""
    trajectory = []
    for msg in messages:
        role = msg.get("role", "")
        content = msg.get("content", "")

        # Role mapping
        if role == "system":
            from_field = "system"
        elif role == "user":
            from_field = "human"
        elif role == "assistant":
            from_field = "gpt"
            # Tool calls: wrap in <tool_call> tags
            if msg.get("tool_calls"):
                for tc in msg["tool_calls"]:
                    fn = tc.get("function", {})
                    tc_text = json.dumps({
                        "name": fn.get("name", ""),
                        "arguments": json.loads(fn.get("arguments", "{}")),
                    }, ensure_ascii=False)
                    content += f"\n<tool_call>\n{tc_text}\n</tool_call>"
        elif role == "tool":
            from_field = "tool"
            # Wrap in <tool_response> tags
            tool_id = msg.get("tool_call_id", "")
            content = (f"<tool_response>\n"
                       f'{{"tool_call_id": "{tool_id}", '
                       f'"content": {json.dumps(content, ensure_ascii=False)}}}\n'
                       f"</tool_response>")
        else:
            continue

        trajectory.append({"from": from_field, "value": content})

    return trajectory

Bước 2: Công cụ trích xuất thống kê

def extract_tool_stats(messages: list[dict]) -> dict:
    """Count tool usage: how many calls, successes, failures per tool."""
    stats: dict[str, dict] = {}
    # Find all tool_call -> tool pairs
    tool_calls = {}
    for msg in messages:
        if msg.get("role") == "assistant" and msg.get("tool_calls"):
            for tc in msg["tool_calls"]:
                fn = tc.get("function", {})
                tool_calls[tc["id"]] = fn.get("name", "unknown")
        elif msg.get("role") == "tool":
            tc_id = msg.get("tool_call_id", "")
            tool_name = tool_calls.get(tc_id, "unknown")
            if tool_name not in stats:
                stats[tool_name] = {"count": 0, "success": 0, "failure": 0}
            stats[tool_name]["count"] += 1
            content = msg.get("content", "")
            if "error" in content.lower()[:100]:
                stats[tool_name]["failure"] += 1
            else:
                stats[tool_name]["success"] += 1
    return stats

Bước 3: Thu thập hàng loạt (batch_runner đơn giản hóa)

def run_batch(prompts: list[str], output_path: str):
    """Run agent on each prompt, collect trajectories."""
    results = []
    for i, prompt in enumerate(prompts):
        conn = init_db(":memory:")
        session_id = create_session(conn)
        cached_prompt = build_system_prompt(os.getcwd())

        try:
            result = run_conversation(prompt, conn, session_id, cached_prompt)
            messages = result["messages"]
            trajectory = convert_to_trajectory(messages)
            tool_stats = extract_tool_stats(messages)

            results.append({
                "prompt_index": i,
                "trajectory": trajectory,
                "tool_stats": tool_stats,
                "completed": result.get("final_response") is not None,
                "api_calls": len([m for m in messages if m.get("role") == "assistant"]),
            })
        except Exception as e:
            results.append({
                "prompt_index": i,
                "trajectory": [],
                "completed": False,
                "error": str(e),
            })
        finally:
            conn.close()

        print(f"  [{i+1}/{len(prompts)}] {'OK' if results[-1].get('completed') else 'FAIL'}")

    # Write to JSONL
    with open(output_path, "w") as f:
        for r in results:
            f.write(json.dumps(r, ensure_ascii=False) + "\n")

    ok = sum(1 for r in results if r.get("completed"))
    print(f"\nBatch complete: {ok}/{len(prompts)} succeeded -> {output_path}")
    return results

Bước 4: Nén quỹ đạo (Đơn giản hóa)

def compress_trajectory(
    trajectory: list[dict],
    target_tokens: int = 15250,
    protect_last_n: int = 4,
) -> tuple[list[dict], dict]:
    """
    Compress a trajectory to fit within target token budget.

    Strategy: protect head (system + first human + first gpt) and
    tail (last N turns). Compress middle into a summary.

    Returns (compressed_trajectory, metrics).
    """
    original_tokens = estimate_tokens_trajectory(trajectory)
    if original_tokens <= target_tokens:
        return trajectory, {
            "was_compressed": False,
            "original_tokens": original_tokens,
            "compressed_tokens": original_tokens,
        }

    # Protect head: system + first human + first gpt/tool
    head = []
    rest = list(trajectory)
    for role in ["system", "human", "gpt"]:
        for i, turn in enumerate(rest):
            if turn["from"] == role:
                head.append(rest.pop(i))
                break

    # Protect tail
    tail = rest[-protect_last_n:] if len(rest) > protect_last_n else []
    middle = rest[:-protect_last_n] if tail else rest

    # Compress middle section into a summary
    if middle:
        summary_text = _summarize_middle(middle)
        compressed_middle = [{"from": "system", "value": f"[Summary of {len(middle)} turns]\n{summary_text}"}]
    else:
        compressed_middle = []

    compressed = head + compressed_middle + tail
    compressed_tokens = estimate_tokens_trajectory(compressed)

    return compressed, {
        "was_compressed": True,
        "original_tokens": original_tokens,
        "compressed_tokens": compressed_tokens,
        "turns_removed": len(middle),
    }


def _summarize_middle(turns: list[dict]) -> str:
    """Generate a concise summary of the middle turns."""
    # Teaching version uses rule-based summary; production version uses LLM
    tools_used = set()
    errors = 0
    for t in turns:
        if "<tool_call>" in t.get("value", ""):
            import re as _re
            names = _re.findall(r'"name":\s*"(\w+)"', t["value"])
            tools_used.update(names)
        if t["from"] == "tool" and "error" in t.get("value", "").lower():
            errors += 1

    parts = [f"Agent executed {len(turns)} turns."]
    if tools_used:
        parts.append(f"Tools used: {', '.join(sorted(tools_used))}.")
    if errors:
        parts.append(f"Encountered {errors} error(s) and recovered.")
    return " ".join(parts)


def estimate_tokens_trajectory(trajectory: list[dict]) -> int:
    """Rough token estimate for a trajectory."""
    total_chars = sum(len(t.get("value", "")) for t in trajectory)
    return total_chars // 4  # rough estimate

Hướng dẫn: Vòng đời đầy đủ của một quỹ đạo

=== Phase 1: Collection ===

prompt: "Write a Python script to convert Markdown to HTML"

Agent executes:
  Turn 1: [gpt] <think>I should use the markdown library...</think> I'll write a script.
  Turn 2: [gpt] <tool_call>{"name": "terminal", "arguments": {"command": "pip install markdown"}}</tool_call>
  Turn 3: [tool] <tool_response>{"content": "Successfully installed markdown-3.6"}</tool_response>
  Turn 4: [gpt] <tool_call>{"name": "write_file", ...}</tool_call>
  Turn 5: [tool] <tool_response>{"content": "Written 120 chars"}</tool_response>
  Turn 6: [gpt] <tool_call>{"name": "terminal", "arguments": {"command": "python convert.py test.md"}}</tool_call>
  Turn 7: [tool] <tool_response>{"content": "<h1>Hello</h1>"}</tool_response>
  Turn 8: [gpt] Script is ready and tests pass.

tool_stats: terminal(2/2), write_file(1/1)
reasoning_stats: 1/4 turns with <think>
completed: true

=== Phase 2: Compression ===

Original: 8 turns, ~3,000 tokens -> no compression needed (< 15,250)
(For a complex conversation with 50K tokens, Turns 3-5 would be compressed into a summary)

=== Phase 3: Scoring ===

correctness_reward: script runs and output is correct -> 2.0
format_reward: has <think> and <tool_call> -> 0.5
total_reward: 2.5

=== Phase 4: Training ===

GRPO receives multiple trajectories for the same prompt (e.g., 4):
  Trajectory A: reward 2.5 (success + good format)
  Trajectory B: reward 2.0 (success but no think)
  Trajectory C: reward 0.5 (failure but correct format)
  Trajectory D: reward 0.0 (failure + bad format)

Model update direction: reinforce behavior patterns from A and B, weaken C and D

Tại sao không chỉ sử dụng SFT (Tinh chỉnh được giám sát)

SFT chỉ học từ "câu trả lời đúng". Nhưng các cuộc trò chuyện của nhân viên hỗ trợ chứa một lượng lớn hành vi khám phá -- thử và sai, quay lại, chuyển đổi phương pháp tiếp cận -- những hành vi không có "câu trả lời đúng", chỉ có mức độ "tốt hay xấu".

Ưu điểm của RL: nó có thể học được sự khác biệt giữa "gần đúng" và "hoàn toàn sai". SFT chỉ có hai trạng thái: "đã học" và "không học được".

SFTRL (GRPO)
Tín hiệuNhị phân: đúng/saiLiên tục: điểm thưởng
Nó học được gìBắt chước quỹ đạo chính xácHọc hỏi từ những so sánh tốt/xấu
Thái độ đối với việc khám pháBỏ qua nóKhám phá thất bại cũng có giá trị học hỏi
Hiệu quả dữ liệuYêu cầu câu trả lời đúng chất lượng caoMọi quỹ đạo đều có thể sử dụng được (miễn là có phần thưởng)

Những lỗi thường gặp của người mới bắt đầu

1. Không lọc ra các mẫu không có lý do

Nếu agent tạo ra một quỹ đạo không có <think> lý luận nào cả, quỹ đạo đó dạy cho mô hình "bạn có thể hành động mà không cần suy nghĩ".

Khắc phục: batch_runner tự động loại bỏ các mẫu trong đó has_any_reasoning=false.

2. Nén đầu hoặc đuôi

Phần đầu (lời nhắc hệ thống + câu hỏi của người dùng) xác định nhiệm vụ là gì. Phần đuôi (vài lượt cuối) chứa kết quả cuối cùng. Nén đi một trong hai và mô hình sẽ học được "một quy trình trung gian không có mục tiêu".

Khắc phục: Protect_first_system/human/gpt + Protect_last_n_turns không bao giờ bị nén.

3. Chức năng khen thưởng chỉ kiểm tra tính đúng đắn

Chỉ chấm điểm bằng "kết quả cuối cùng có đúng không." Mô hình học được rằng "đoán đúng mới là quan trọng" -- nó sẽ bỏ qua việc suy luận và chuyển ngay sang việc đoán câu trả lời.

Khắc phục: Xếp chồng định dạng_phần thưởng lên trên, khen thưởng cho định dạng lý luận tốt và trừng phạt các quỹ đạo không có <think>.

4. Chỉ chạy mỗi lời nhắc một lần

GRPO cần so sánh nhiều quỹ đạo cho cùng một dấu nhắc. Chạy một lần không có gì để so sánh.

Khắc phục: Chạy mỗi lời nhắc ít nhất 2 lần (có thể định cấu hình trong batch_runner).

Ranh giới giảng dạy

Chương này bao gồm bốn điều:

  1. Định dạng quỹ đạo -- Định dạng ShareGPT, thẻ lý do, thẻ tool_call/tool_response
  2. Bộ sưu tập hàng loạt -- đường dẫn dẫn đến quỹ đạo nhanh chóng của batch_runner
  3. Nén quỹ đạo -- Bảo vệ đầu và đuôi, nén phần giữa, ngân sách token
  4. Kiến thức cơ bản về đào tạo RL -- Chức năng khen thưởng, cách tiếp cận GRPO, tại sao chỉ SFT là không đủ

Không được bảo hiểm:

  • Đạo hàm toán học của GRPO -> Khóa học lý thuyết RL
  • API cụ thể cho Atropos/Tinker -> tích hợp nền tảng
  • Cấu hình WandB và giải thích số liệu -> Chi tiết MLOps
  • Đào tạo đa GPU và lập kế hoạch phân phối -> cơ sở hạ tầng
  • Nguồn dữ liệu cụ thể và làm sạch -> kỹ thuật dữ liệu

Chương này liên quan đến chương khác như thế nào

  • Vòng lặp cốt lõi của s01 -> mỗi quỹ đạo là một bản ghi hoàn chỉnh của một quỹ đạo run_conversation gọi
  • Nén ngữ cảnh của s05 -> nén quỹ đạo sử dụng lại chiến lược "bảo vệ đầu và đuôi, nén ở giữa" tương tự
  • Đánh giá lý lịch của s20 -> tìm hiểu runtime (sửa đổi tệp); s23 đang học offline (sửa đổi mô hình)
  • Sáng tạo kỹ năng của s21 -> tiến hóa runtime; s23 là tiến hóa cấp độ di truyền
  • Kiến trúc plugin của s24 -> Môi trường RL có thể được mở rộng dưới dạng plugin

s20-s21 thể hiện quá trình "học tập" của agent; s23 tượng trưng cho sự "tiến hóa" của agent Việc học làm thay đổi kiến ​​thức; sự tiến hóa tự thay đổi khả năng.

Sau chương này, bạn sẽ có thể trả lời

  • Tại sao lý luận được gói gọn trong <think> thẻ ở định dạng quỹ đạo?
  • Tại sao đầu và đuôi không thể nén được trong quá trình nén quỹ đạo?
  • Tại sao GRPO cần nhiều quỹ đạo cho cùng một dấu nhắc?
  • Điều gì sẽ xảy ra nếu chức năng khen thưởng chỉ đo lường tính đúng đắn?
  • Học trong runtime (s20-s21) giải quyết được cấp độ vấn đề nào so với đào tạo ngoại tuyến (s23)?

Một điều: Quỹ đạo cuộc trò chuyện là bản ghi kinh nghiệm của tổng đài viên. batch_runner thu thập chúng, axis_compressor nén chúng và GRPO học hỏi từ chúng -- làm cho khả năng của mô hình thế hệ tiếp theo trở nên mạnh mẽ hơn chứ không chỉ hiểu biết nhiều hơn.

Mục lục nguồn