---
title: "人工智能实训结题报告：B4 Agent LLM决策模块 —— 从架构设计到五维进阶"
abbrlink: ai-training-b4-final-report
date: "2026-07-17 13:30:00"
updated: "2026-07-17 13:30:00"
desc: 综合实训Ⅱ阶段个人结题技术报告。B4 LLM决策模块完整记录：双保险Prompt策略、三层级联容错解析、五项进阶功能（多工具并发、Plan-and-Execute、模型切换、Schema传参对比、批量基准测试），Qwen3.5-4B实测6/6成功率100%。包含系统架构图、核心代码、性能评估与实训心得。
categories:
    - 实习
tags:
    - Agent
    - LLM
    - Qwen
    - B4
    - 结题报告
    - 人工智能实训
    - Prompt工程
    - 工具调用
cover: /AI/signature.webp
---

> 本文为综合实训Ⅱ阶段个人结题技术报告，原为校内验收文档，现整理为博客公开版本。对团队其他成员姓名做了打码处理。
>
> **个人 GitHub 仓库**：[https://github.com/xingwangzhe/B4-Agent-LLM](https://github.com/xingwangzhe/B4-Agent-LLM)
>
> **团队合并仓库**：[https://github.com/woaiwang/Voice-Agent/](https://github.com/woaiwang/Voice-Agent/)
>
> 实训系列博文：[Day1 环境搭建](https://xingwangzhe.fun/posts/ai-training-ubuntu-conda-day1/) → [Day2 SFT与DPO](https://xingwangzhe.fun/posts/ai-training-sft-dpo-day2/) → [Day3 Agent实践](https://xingwangzhe.fun/posts/ai-training-agent-day3/) → [Day4 Proposal](https://xingwangzhe.fun/posts/ai-training-agent-day4-proposal/) → [Week2 五维升级](https://xingwangzhe.fun/posts/ai-training-b4-llm-week2/) → [验收讲解](https://xingwangzhe.fun/posts/ai-training-b4-week2-review/)

---

![实习证明](/AI/signature.webp)

## 一、 项目与团队基本信息

- **本人姓名**：王兴家
- **本人学号**：2023xxxx
- **项目名称**：Agent智能体系统（方向B）
- **实际完成目标**：包含进阶挑战项（完成了 5 项进阶功能：①单轮多 tool_calls 与多 ToolMessage ②Plan-and-Execute 模式 ③多模型切换 ④tools_schema 传参方式对比（prompt 注入 vs 内置传参）⑤批量测试 + 工具调用成功率/Token 统计）
- **小组其他成员**：查同学、张同学、计同学

### 成员最终分工与交付核对表

| 角色 |    姓名    |     学号     |    实际负责的核心模块名称    |                   个人代码库链接                   |
| :--: | :--------: | :----------: | :--------------------------: | :------------------------------------------------: |
| 组长 |   查同学   |   2023xxxx   | B1 - Agent运行与消息管理模块 |     <https://github.com/woaiwang/Voice-Agent/>     |
| 组员 |   张同学   |   2023xxxx   | B3 - 说明生成与工具调用模块  | <https://github.com/kisssh00000000t/b3_tool_layer> |
| 组员 |   计同学   |   2023xxxx   |    B2 - Skill工具函数模块    |       <https://github.com/dsi-co/Agent_b_b2>       |
| 组员 | **王兴家** | **2023xxxx** |  **B4 - Agent LLM决策模块**  |   <https://github.com/xingwangzhe/B4-Agent-LLM>    |

---

## 二、 整体系统架构与最终成果展示

### 2.1 最终系统总体架构图

下图展示了本项目五个模块（B1-B5）在系统中的物理位置与数据流向：

```mermaid
graph TD
    subgraph 用户层
        User[("👤 用户")]
    end

    subgraph 运行时编排层
        B1["B1 Agent Runtime<br/>运行与消息管理"]
    end

    subgraph 决策与记忆层
        B4["B4 LLM Decision<br/>模型决策模块"]
        B5["B5 Memory<br/>记忆文档存储与查找"]
    end

    subgraph 工具桥接与执行层
        B3["B3 Tool Layer<br/>说明生成与工具调用"]
        B2["B2 Skill<br/>工具函数实现"]
    end

    subgraph 基础设施
        CFG["配置文件<br/>model.yaml<br/>tools.yaml<br/>memory.yaml"]
        LLM["本地模型<br/>Qwen3.5-4B"]
    end

    User -->|"自然语言指令<br/>runtime_input.json"| B1

    B1 -->|"① 查询记忆"| B5
    B5 -->|"记忆上下文"| B1

    B1 -->|"② 请求工具说明"| B3
    B3 -->|"tools_schema"| B1

    B1 -->|"③ messages + schema<br/>generate_ai_message()"| B4
    B4 -->|"AIMessage<br/>(content/tool_calls)"| B1
    B4 -->|"加载推理"| LLM

    B1 -->|"④ 执行tool_calls"| B3
    B3 -->|"动态加载调用"| B2
    B2 -->|"SkillResult"| B3
    B3 -->|"ToolMessage"| B1

    B1 -->|"⑤ 保存记忆"| B5
    B1 -->|"最终回答"| User

    B1 -->|"读取配置"| CFG
    B3 -->|"读取配置"| CFG
    B4 -->|"读取配置"| CFG
    B5 -->|"读取配置"| CFG
```

**模块职责说明：**

| 模块                  | 角色         | 核心职责                                                                                                                                                |
| :-------------------- | :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **B1**（组长-查同学） | 运行时编排层 | 唯一编排者与消息中枢，基于`messages[-1]`角色驱动三状态ReAct循环（准备输入→调用LLM→执行工具），支持断点恢复、上下文压缩、多轮输入与动态System Prompt切换 |
| **B2**（计同学）      | 工具函数层   | 实现5个基础Skill：`calculator`、`file_reader`、`local_file_search`、`table_analyzer`、`format_converter`，通过`importlib`动态加载、`inspect`签名注入    |
| **B3**（张同学）      | 工具桥接层   | 将B2函数转为OpenAI function calling格式schema，校验tool_call参数合法性，动态加载执行并返回ToolMessage，支持IO异常有限重试                               |
| **B4**（王兴家）      | 认知决策层   | 唯一与大模型交互的模块，加载本地Qwen3.5-4B，接收messages+tools_schema，输出AIMessage。双保险Prompt策略+三层级联解析，content优先修复互斥冲突            |
| **B5**                | 记忆系统层   | 基于Markdown文件+JSON索引的本地记忆系统，支持按ID检索+全局记忆合并+`max_memory_chars`截断                                                               |

### 2.2 系统整体运行流程与集成说明

#### 三阶段 ReAct 闭环

系统整体遵循 ReAct（Reasoning + Acting）范式，由 B1 运行时编排器驱动一个完整的三阶段闭环：

```mermaid
sequenceDiagram
    participant U as 用户
    participant B1 as B1 Runtime
    participant B5 as B5 Memory
    participant B3 as B3 Tool Layer
    participant B4 as B4 LLM决策
    participant B2 as B2 Skill

    Note over B1,B5: 阶段 0：初始化
    B1->>B5: 查询历史记忆
    B5-->>B1: 返回记忆上下文文档
    B1->>B3: 请求工具说明
    B3-->>B1: 返回 OpenAI 格式 tools_schema

    loop ReAct 循环（直至无 tool_calls 或达 max_turns）
        Note over B1,B4: 阶段 1：LLM 决策
        B1->>B4: generate_ai_message(messages, tools_schema)
        Note over B4: Prompt注入 → 模型推理 → 输出解析
        B4-->>B1: AIMessage（content 或 tool_calls）

        alt 有 tool_calls
            Note over B1,B2: 阶段 2：工具执行
            B1->>B3: execute_tool_calls(tool_calls)
            B3->>B2: 动态加载并调用 Skill 函数
            B2-->>B3: SkillResult（status + output）
            B3-->>B1: ToolMessage（结果封装）
            B1->>B1: 追加 ToolMessage 到 messages
        else 无 tool_calls（content 非空）
            Note over B1: 阶段 3：输出结果
            B1->>U: 返回最终回答
        end
    end

    Note over B1,B5: 收尾：记忆保存
    B1->>B5: save_memory(对话记录)
```

**详细执行流程：**

1. **初始化阶段**：B1读取`runtime_input.json`，加载系统提示词。调用B5检索历史记忆并注入到messages中。调用B3读取`tools.yaml`生成`tools_schema.json`（OpenAI function calling格式）。

2. **ReAct 循环**（状态机驱动）：
    - **决策步**（Stage 1）：当messages末尾为user或tool时，B1调用B4的`generate_ai_message(messages, tools_schema)`。B4加载本地Qwen3.5-4B模型进行推理，输出AIMessage（含tool_calls或content）。若返回content（无tool_calls），则结束循环输出最终回答。
    - **工具步**（Stage 2）：当AIMessage含tool_calls时，B1调用B3的`execute_tool_calls()`，B3解析tool_call参数、动态加载B2对应的Skill函数并执行，返回封装好的ToolMessage。B1将其追加到messages，检查是否达到`max_turns`，未达到则返回决策步。
    - **状态持久化**：每次状态变更后，B1将messages、trace、final_answer实时写入磁盘，支持断点恢复。

3. **收尾阶段**：生成最终答案`final_answer.md`，调用B5.save_memory()将本次对话保存为记忆。

#### 模块间接口契约

所有模块采用统一的JSON数据格式（基于OpenAI chat completions标准）：

| 消息类型    | 关键字段                                                            | 用途                     |
| :---------- | :------------------------------------------------------------------ | :----------------------- |
| AIMessage   | `role: "assistant"`, `content`, `tool_calls: [{id, name, args}]`    | B4输出，B1据此判断下一步 |
| ToolMessage | `role: "tool"`, `tool_call_id`, `name`, `content`(SkillResult JSON) | B3输出，封装工具执行结果 |
| SkillResult | `skill_name`, `status`, `input`, `output`, `error`, `latency_ms`    | B2输出，B3消费           |

#### 集成过程中遇到的问题与解决

在模块合并联调时，主要遇到了以下问题：

1. **JSON字段不一致**：初始联调时B1以OpenAI格式传递messages，但部分字段名和嵌套结构与B4/B3预期不匹配。最终通过统一定义`common/schemas.py`中的`make_ai_message`、`validate_ai_message`、`validate_messages`三个核心函数，对所有模块间的数据交换进行严格校验，解决了此问题。

2. **B4与B3数据格式分歧**：B4输出的tool_calls中工具名称使用"name"字段，但B3在动态加载Skill时需要"function.name"格式。通过在B3侧增加字段归一化层，自动兼容两种命名风格。

3. **Mock模式与真实模型的行为差异**：在Mock模式下联调通过的全部场景，切换到真实Qwen3.5-4B模型后出现了模型输出解析失败的情况。最终通过增强B4的JSON解析从严格互斥改为静默修复后，切换回真实模型时部分场景从失败变为成功。

4. **B1断点恢复的"坏消息回滚"机制**：联调中发现当LLM解析失败时，messages中残留的无效assistant消息会导致后续重试失败。B1实现了自动检测与回滚——若trace状态为`llm_parse_error`且末尾角色为assistant，则自动弹出该消息并重试。

### 2.3 最终产品展示 (Demo)

下图展示了 **Voice Agent 全链路闭环**：用户通过语音输入"什么是智能体？"，系统经过语音识别 → Agent 运行时编排 → LLM 决策 → 工具调用，最终输出关于智能体定义、特征和应用的详细回答，完整覆盖 B1→B4→B3→B2→B5 全模块链路：

![Voice Agent 全链路闭环：语音输入"什么是智能体"，系统输出详细回答](/AI/B_system_voice_agent_full_answer.webp)

下图展示了**动态 System Prompt 切换**功能：Round 1 使用默认身份计算 16+16，Round 2 动态切换为"古文风格"身份后追问"刚才的结果是多少？"，模型以"十加十得二十，此乃算术之常理"作答，验证了系统运行时的动态上下文切换能力：

![动态 System Prompt 切换：Round1 默认身份 → Round2 切换古文风格](/AI/B_system_dynamic_prompt_switch.webp)

### 2.4 团队系统代码库

- **团队 Github 开源仓库链接**：<https://github.com/woaiwang/Voice-Agent/>

---

## 三、 个人核心模块技术报告（B4 - Agent LLM决策模块）

### 3.1 模块定位与系统融合方式

- **在系统中的角色**：B4（LLM 决策模块）是整个 Agent 系统的**唯一与大模型交互的模块**，相当于系统的"大脑 / 决策中枢"。它把 B1 编排器给过来的对话上下文，转化为结构化决策——要么是需要调用工具的 `tool_calls`，要么是直接回复用户的 `content`。没有 B4，系统就无法把用户自然语言指令转成可执行的决策，B1 的编排循环与 B3 的工具执行都无从驱动。

- **上下游依赖与接口协同**：
    - **上游（输入）**：由 **B1 运行时编排器**调用 `generate_ai_message(messages, tools_schema)`。接收两类数据：
        - `messages`：OpenAI chat 格式消息序列（含 system / user / assistant / tool 四种角色），由 B1 维护；
        - `tools_schema`：由 **B3** 根据 `tools.yaml` 生成的 OpenAI function calling 格式工具说明。
    - **下游（输出）**：返回标准 `AIMessage`（`{"role":"assistant","content":..., "tool_calls":[{"id","name","args"}]}`）给 **B1**。B1 据此判断：有 `tool_calls` 则交给 B3 执行→`ToolMessage`→追加回 messages→再次调用 B4；无 `tool_calls`（content 非空）则输出 `final_answer`，结束循环。
    - **接口契约**：模块对外严格遵守团队统一的数据契约——通过 `common/schemas.py` 的 `make_ai_message` / `validate_ai_message` / `validate_messages` 对 `messages`、`AIMessage`、`ToolMessage`、`SkillResult` 进行构造与校验，从根源上避免了 B1/B3/B4 之间 JSON 字段不一致的问题。

- **输入输出格式示例**：

    ```json title="B4 输入输出格式示例"
    # B4 接收的输入 (messages)
    [
      {"role": "system", "content": "你是本地工具调用Agent..."},
      {"role": "user", "content": "帮我阅读docs/agent_intro.txt，总结三条中文要点"}
    ]

    # 同时接收 tools_schema (OpenAI function calling 格式):
    [
      {
        "type": "function",
        "function": {
          "name": "file_reader",
          "description": "读取本地UTF-8文本或Markdown文件",
          "parameters": {
            "type": "object",
            "properties": {
              "path": {"type": "string", "description": "文件路径"},
              "max_chars": {"type": "integer", "description": "最大读取字符数"}
            },
            "required": ["path"]
          }
        }
      }
    ]

    # B4 返回的 AIMessage (决策调用工具):
    {
      "content": "",
      "tool_calls": [
        {"id": "call_001", "name": "file_reader", "args": {"path": "docs/agent_intro.txt", "max_chars": 2000}}
      ]
    }

    # B4 返回的 AIMessage (工具结果后直接回答):
    {
      "content": "三条要点：\n1. Agent智能体系统由模型、工具、记忆和执行循环四个核心部分组成...",
      "tool_calls": []
    }
    ```

### 3.2 核心技术实现路径

#### 技术栈选型

| 类型         | 名称                            | 用途                                                       |
| :----------- | :------------------------------ | :--------------------------------------------------------- |
| 语言/运行时  | **Python 3.10**                 | 模块开发，类型提示完善                                     |
| 基础模型     | **Qwen3.5-4B-Instruct**         | 本地 LLM 推理，4B 参数，中文能力强                         |
| 推理框架     | **HuggingFace Transformers**    | 模型加载与推理（`AutoModelForCausalLM` + `AutoTokenizer`） |
| 深度学习后端 | **PyTorch 2.x (CUDA)**          | 模型推理后端，`bfloat16` 精度，`device_map="auto"`         |
| 配置解析     | **PyYAML**                      | `model.yaml` 解析，代码与配置分离                          |
| 数据序列化   | **json（标准库）**              | 输入输出标准化与落盘持久化                                 |
| 解码策略     | **`do_sample=false`**           | 贪心解码，确保工具调用输出的确定性                         |
| 模型路径     | `/root/assignment_B/Qwen3.5-4B` | 本地离线模型，`local_files_only=true`                      |

#### 算法与工程实现

- **模型与框架**：基于本地部署的 **Qwen3.5-4B**（HuggingFace `transformers` 加载，`torch_dtype=bfloat16`，`device_map="auto"`，`max_new_tokens=1024`，`do_sample=False`），不依赖任何外部 API，全过程离线推理。`model.yaml` 配置了两个命名 profile：`qwen-4b`（标准模式）和 `qwen-4b-fast`，通过 `--model_name` 动态切换。

- **双保险 Prompt 策略（关键设计一）**：`_build_prompt_messages()` 在 System 消息里注入完整的"输出格式契约"（强制单一 JSON `{content, tool_calls}`、禁止 Markdown / 反引号 / 解释文本），并在**最后一条 user / tool 消息**尾部追加 `envelope_reminder` 二次强化约束，显著提升小模型稳定输出可解析 JSON 的概率。核心代码实现如下：

    ```python title="_build_prompt_messages() — 双保险Prompt策略"
    def _build_prompt_messages(messages, tools_schema):
        prompt_messages = deepcopy(messages)
        # 第一保险：System Message 中注入完整格式说明
        format_instruction = (
            "IMPORTANT OUTPUT FORMAT:\n"
            "You must return exactly one valid JSON object.\n"
            "Do not output markdown / explanations / code fences.\n"
            'Valid schema A (final answer): {"content":"...","tool_calls":[]}\n'
            'Valid schema B (tool call): {"content":"","tool_calls":[{"id":"...",...}]}\n'
            "You may include zero, one, or multiple tool_calls."
        )
        system_instruction = "\n\nAvailable tools:\n" + json.dumps(tools_schema, ensure_ascii=False) + "\n" + format_instruction
        if prompt_messages and prompt_messages[0].get("role") == "system":
            prompt_messages[0]["content"] += system_instruction
        else:
            prompt_messages.insert(0, {"role": "system", "content": system_instruction.strip()})
        # 第二保险：追加到最后一个 User Message
        envelope_reminder = "IMPORTANT: Output the JSON object now. First char must be '{', last must be '}'. No backtick/Markdown."
        for message in reversed(prompt_messages):
            if message.get("role") == "user":
                message["content"] += "\n\n" + envelope_reminder
                break
        # ToolMessage 特殊处理：自动追加引导 prompt
        if prompt_messages and prompt_messages[-1].get("role") == "tool":
            tool_count = sum(1 for m in reversed(prompt_messages) if m.get("role") == "tool")
            prompt_messages.append({"role": "user", "content": envelope_reminder + f" The last {tool_count} ToolMessage(s) contain results. If info is sufficient, answer with schema A now."})
        return prompt_messages
    ```

- **三层级联 JSON 解析（关键设计二）**：真实 Qwen3.5-4B 的输出并不总是一段干净 JSON，常出现前后夹杂解释文本、尾随反引号、甚至 `tool_calls` 数组被截断等情况。为此实现了 `_parse_model_output()` 的级联兜底：
    1. `json.loads` 严格解析（最快路径）；
    2. `_extract_json_object` 逐字符扫描，定位第一个合法 JSON 对象；
    3. `_parse_json_with_backtick_tail` 处理尾随 `` ` `` 的情况；
    4. `_parse_tool_calls_fragment` 兜底解析残缺的 `tool_calls` 数组。

- **content / tool_calls 互斥归一化（关键设计三）**：在 `_candidate_to_message()` 中，若模型同时给出 content 与 tool_calls，按"**content 优先**"原则清空 tool_calls，避免"既回答又调用工具"的冲突（这是 Mock→真实模型切换后大量场景从失败变成功的关键修复）。

- **模型缓存单例（关键设计四）**：模块级全局缓存 `_MODEL_CACHE`，以模型路径 / 精度 / device_map / local_only 等 7 个参数的元组为 key 缓存 `(tokenizer, model)`，同进程内重复调用直接命中缓存（约 237ms 加载 vs 首次 7-10s），批量测试时收益尤为显著。切换 `--model_name` 时路径不同自动 cache miss，无需手动清理：

    ```python title="_MODEL_CACHE — 模型缓存单例"
    _MODEL_CACHE: dict[tuple[str, ...], tuple[Any, Any]] = {}

    def _load_model_bundle(auto_model, auto_tokenizer, model_path, tokenizer_path, **kwargs):
        cache_key = (str(model_path), str(tokenizer_path),
                     str(kwargs.get("local_only")), str(kwargs.get("trust_remote_code")),
                     str(kwargs.get("dtype")), str(kwargs.get("device_map")),
                     str(kwargs.get("max_memory")))
        cached = _MODEL_CACHE.get(cache_key)
        if cached is not None:
            return cached  # cache hit
        tokenizer = auto_tokenizer.from_pretrained(...)
        model = auto_model.from_pretrained(...)
        _MODEL_CACHE[cache_key] = (tokenizer, model)
        return tokenizer, model
    ```

- **Mock 模式（关键设计五）**：`_mock_generate()` 在无 GPU/模型环境下提供确定性输出，支持 CI 测试和无 GPU 联调。首次调用模拟并发返回 `file_reader` + `local_file_search` 两个 `tool_calls`；获取 ToolMessage 后根据执行结果分支——全部成功则汇总为三条中文要点，任一失败则返回错误消息。确定性输出使集成调试迭代速度大幅提升。

- **关键代码逻辑**：下面两段最能体现本模块的工作量与技术思考。

    **① 三层级联容错解析（保证任何脏输出都能尽量解析出决策）**

    ```python title="_parse_model_output() — 三层级联容错解析"
    def _parse_model_output(raw_text: str) -> tuple[dict, dict]:
        try:
            candidate = json.loads(raw_text.strip())
        except json.JSONDecodeError as exc:
            # 尝试从脏文本中提取第一个合法 JSON 对象
            extracted = _extract_json_object(raw_text)
            if extracted is not None:
                try:
                    return _candidate_to_message(extracted)
                except Exception:
                    pass
            # 依次尝试：尾随反引号剥离 → tool_calls 残缺数组兜底
            try:
                candidate = _parse_json_with_backtick_tail(raw_text, exc)
            except json.JSONDecodeError:
                candidate = _parse_tool_calls_fragment(raw_text, exc)
        return _candidate_to_message(candidate)
    ```

    **② content 优先于 tool_calls 的互斥归一化（静默修复冲突）**

    ```python title="_candidate_to_message() — content优先互斥归一化"
    content = candidate.get("content", "")
    tool_calls = candidate.get("tool_calls", [])
    # 规范化：两者都非空时，优先使用 content，清空 tool_calls
    if content and tool_calls:
        print("⚠️ 警告：模型同时提供了 content 和 tool_calls，已忽略 tool_calls，使用 content 作为最终回答。",
              file=sys.stderr, flush=True)
        tool_calls = []   # 清空，使最终回答优先
    message = {"role": "assistant", "content": content, "tool_calls": tool_calls}
    ```

- **进阶挑战攻克（5 项全部完成）**：

    #### 进阶①：单轮多 tool_calls 与多 ToolMessage

    **问题**：原始 Prompt 约束为"choose exactly one tool"，每轮只能调用一个工具，无法处理需同时读取多个文件或并发执行多个计算的场景。

    **方案**：将 Prompt 中的单工具约束改为"zero, one, or multiple"；`_candidate_to_message()` 支持解析任意数量的 `tool_calls` 数组；`_mock_generate()` 模拟并发输出；提供 3 个和 5 个并发的极限测试数据。

    **效果**：真实 Qwen3.5-4B 的 `case_multi_tool` 场景成功单轮生成 **3 个并发 tool_calls**（`file_reader` + `calculator` + `local_file_search`），显著减少多步骤任务轮次交互。

    #### 进阶②：Plan-and-Execute 模式

    **问题**：标准 ReAct 模式每步只做一次决策，缺乏全局规划，复杂任务易陷入局部最优。

    **方案**：设计双阶段流程——Phase 1 生成结构化计划（`reasoning` + `plan` 数组），Phase 2 按计划逐步执行。新增 4 个核心函数：`_build_plan_prompt_messages()`、`_parse_plan_output()`、`_build_plan_step_prompt_messages()`、`_mock_plan_execute()`。

    **效果**：真实 Qwen3.5-4B 模型成功输出 2 步以上的完整计划，Agent 从"走一步看一步"升级为"先规划后执行"。

    #### 进阶③：多模型切换

    **问题**：`model.yaml` 只支持单模型配置，切换模型需手动修改配置文件路径。

    **方案**：在 `model.yaml` 中新增 `models` 命名配置段，预设两个 profile。`_load_model_config()` 增加 `model_name` 参数，CLI 增加 `--model_name` 选项，修改仅约 20 行代码。模型缓存 `_MODEL_CACHE` 天然支持多模型——不同模型路径生成不同 cache key。

    **效果**：一行命令 `--model_name qwen-4b-fast` 即可切换模型配置。

    #### 进阶④：tools_schema 传参方式对比

    **问题**：`tools_schema` 既可通过 Prompt 文本注入（prompt_json），也可通过 HuggingFace 的 `apply_chat_template(tools=...)` 内置传参（builtin）。哪种方式对 Qwen3.5-4B 更有效？

    **方案**：设计 A/B 对比实验：A 组（prompt_json）将 tools_schema 嵌入 system message 文本，引导模型输出纯 JSON；B 组（builtin）通过 `tokenizer.apply_chat_template(tools=tools_schema, ...)` 传递。

    **结果**：

    | 模式            |     成功率     | 平均延迟 | 解析失败原因                                      |
    | :-------------- | :------------: | :------: | :------------------------------------------------ |
    | **prompt_json** | **100%** (6/6) | 7087.5ms | —                                                 |
    | **builtin**     |  **0%** (0/6)  | ~4094ms  | Qwen3.5-4B 输出原生 XML 格式，JSON 解析器无法处理 |

    **效果**：prompt 注入方式以显著优势胜出，最终确定 `prompt_json` 为默认模式。

    #### 进阶⑤：批量测试 + 成功率 / Token 统计

    **问题**：单场景手动测试效率低，无法系统评估模型在不同场景下的工具调用能力。

    **方案**：实现 `b4_batch_benchmark.py` 自动化评测框架，核心能力：
    - 从 `--cases_dir` 扫描所有测试用例 JSON 文件（`bench_cases/` 目录下 6 个场景）
    - 对每个用例调用 `generate_ai_message()` 并收集状态、延迟、工具调用数
    - 输出汇总统计（成功率、平均延迟、每用例详情）和 `benchmark_summary.json`
    - 提供 `--tool_calling` 参数切换 prompt_json / builtin 模式进行对照

    6 个测试用例设计如下：

    | 用例名                | 对应场景                     | 预期行为                    |
    | :-------------------- | :--------------------------- | :-------------------------- |
    | `case_calculator`     | 计算表达式 `3.14 * 5 - 2.5`  | 直接回答或调用 calculator   |
    | `case_direct_answer`  | 无需工具的直接问答           | 输出 content，tool_calls=[] |
    | `case_file_read`      | 调用 file_reader 读取文件    | 单 tool_call                |
    | `case_file_search`    | 调用 local_file_search 搜索  | 单 tool_call                |
    | `case_multi_tool`     | 3 个工具并发调用             | 3 个并发 tool_calls         |
    | `case_table_analyzer` | 调用 table_analyzer 分析 CSV | 单 tool_call                |

    **效果**：一键运行 6 场景自动化测试，成功率和延迟数据精确可复现。

### 3.3 最终结果与性能评估

#### 测试方法

采用**三种测试方法**相结合的策略：

1. **场景化测试**：设计 3 个核心场景（初始 tool_call 生成、基于 ToolMessage 的最终回答、工具调用失败处理），分别用真实模型和 Mock 模式验证。
2. **批量基准测试**：`b4_batch_benchmark.py` 遍历 `bench_cases/` 下 6 个测试用例，覆盖正常、异常、边界、并发等场景，自动统计成功率和平均延迟。
3. **Mock 模式验证**：所有测试用例在 Mock 模式下均达到 100% 成功率，确保代码逻辑正确性独立于模型能力。

#### 结果分析

- **`prompt_json` 模式（真实模型，本次实测）**：在远程 GPU 服务器（NVIDIA H200 NVL）上用本地 **Qwen3.5-4B** 实跑 `b4_batch_benchmark.py`，**6 个场景全部成功，解析成功率 100%（6/6）**，平均延迟 **7087.5 ms**（单 case 范围 2282.5–14963.1 ms）。各场景明细如下：

    | 测试场景              |    状态    |  延迟(ms)   | tool_calls | 说明                                     |
    | :-------------------- | :--------: | :---------: | :--------: | :--------------------------------------- |
    | `case_calculator`     | ✅ success |   10296.6   |     0      | 直接输出 content（content 优先策略生效） |
    | `case_direct_answer`  | ✅ success |   2758.8    |     0      | 直接回答，无需工具                       |
    | `case_file_read`      | ✅ success |   2282.5    |     1      | 调用 file_reader 读取文件                |
    | `case_file_search`    | ✅ success |   8201.7    |     1      | 调用 local_file_search 搜索文本          |
    | `case_multi_tool`     | ✅ success | **14963.1** |   **3**    | **单轮生成 3 个并发 tool_calls**         |
    | `case_table_analyzer` | ✅ success |   4022.5    |     1      | 调用 table_analyzer 分析表格             |

- **`builtin` 模式（对照）**：历史对照测试显示解析成功率为 0%——原因是 Qwen3.5-4B 在 `apply_chat_template(tools=)` 下输出 **XML 格式**而非 JSON，现有解析器仅兼容 JSON。
- **过程可追溯**：每次推理都落盘 `raw_model_output.json`（模型原始输出）+ `ai_message.json`（解析后标准格式）+ `llm_run_log.jsonl`，便于复现与对比。

#### 指标汇总

| 评估维度               | 结果                                                 |
| :--------------------- | :--------------------------------------------------- |
| 基础模块功能           | ✅ 全部完成                                          |
| 进阶①-多工具并发       | ✅ 支持 3-5 并发 tool_calls                          |
| 进阶②-Plan-and-Execute | ✅ 双阶段设计，真实模型验证通过                      |
| 进阶③-多模型切换       | ✅ `--model_name` 参数支持，代码修改 ≤ 20 行         |
| 进阶④-Schema 对比      | ✅ prompt_json **100%** vs builtin 0%                |
| 进阶⑤-批量基准         | ✅ 6 用例自动化框架                                  |
| 模块间联调             | ✅ 通过统一 `common/schemas.py` 解决 JSON 字段不一致 |
| 真实模型解析成功率     | **100%** (6/6, prompt_json 模式)                     |
| 平均推理延迟           | **7087.5 ms**（范围 2282.5–14963.1 ms）              |

**批量基准测试实测截图（`prompt_json` 模式，真实 Qwen3.5-4B）：**

![B4 批量基准测试：运行过程与汇总](/AI/B4_benchmark_summary.webp)

![B4 批量基准测试：6 个场景明细](/AI/B4_benchmark_6cases_detail.webp)

**某次真实运行的 `ai_message.json` 截图（单轮 3 并发 tool_calls）：**

![B4 真实运行解析出的 AIMessage（单轮 3 并发 tool_calls）](/AI/B4_aimessage_multi_tool.webp)

**进阶功能② Plan-and-Execute 模式运行截图：**

![B4 Plan-and-Execute 模式运行结果](/AI/B4_plan_execute_result.webp)

**完整闭环 Demo 运行截图：**

![B4 完整闭环 Demo 运行结果](/AI/B4_full_demo_result.webp)

### 3.4 个人交付物清单

- **个人模块源码仓库**：<https://github.com/xingwangzhe/B4-Agent-LLM>

---

## 四、 实训总结与心得体会

> 本次实训的完整过程已以系列博文形式记录在个人博客上，从 Day1 到最终验收共 6 篇：
> [Day1 Ubuntu与Conda环境搭建](https://xingwangzhe.fun/posts/ai-training-ubuntu-conda-day1/) →
> [Day2 SFT与DPO对齐实践](https://xingwangzhe.fun/posts/ai-training-sft-dpo-day2/) →
> [Day3 Agent工具调用与多技能协作](https://xingwangzhe.fun/posts/ai-training-agent-day3/) →
> [Day4 Proposal设计](https://xingwangzhe.fun/posts/ai-training-agent-day4-proposal/) →
> [Week2 B4五维升级实战](https://xingwangzhe.fun/posts/ai-training-b4-llm-week2/) →
> [验收讲解：从基础到进阶的五维实践](https://xingwangzhe.fun/posts/ai-training-b4-week2-review/)

### 4.1 个人实训收获与挑战

- **实训历程回顾**：这次实训从零开始，三周内走完了"环境搭建→模型训练→Agent开发→模块设计→进阶升级→系统集成"的完整链路。

    Day1 在 H200 GPU 服务器上用 Miniconda 搭建 `ai_infer` 推理环境，从 `pip install torch` 到跑通第一个矩阵乘法 `x @ w`——这个最简单的线性变换，就是所有大模型推理的底层原子操作。在优化 `slow_nn.py` 时，把逐条 for 循环改成整个 batch 的矩阵乘法，运行时间从 48.7 秒降到 2.4 秒，第一次直观感受到"充分利用底层 BLAS/MKL 优化"意味着什么。最后导出 `environment.yml` 锁定依赖版本，这个习惯在后续三周里避免了无数次环境问题（[Day1 笔记](https://xingwangzhe.fun/posts/ai-training-ubuntu-conda-day1/)）。

    Day2 进入模型训练，用 LLaMA-Factory 对 Qwen1.5-0.5B-Chat 做 SFT 监督微调。7,473 道 GSM8K 数学题训练 3 个 epoch，看着 loss 从 0.82 一路降到 0.18。评测 Exact Match 准确率 23.5%，虽然不高，但未经 SFT 的 0.5B 模型准确率可能连 5% 都不到。接着做 DPO 偏好优化，Reward Margin 从接近 0 涨到约 30。但 SFT vs DPO 在 100 题上对比的结果让我吃了一惊——SFT 4%，DPO 仅 1%。这个反直觉的结果让我学到了一条关键教训：**评价指标必须匹配训练目标**，DPO 优化的是偏好排序而不是 Exact Match。后来的 CoT Prompt 实验中，5 种模板测下来，最简单的 basic 模板反而效果最好（7%），复杂的 self_check 只有 4%——对小模型而言，简洁比花哨更有效（[Day2 笔记](https://xingwangzhe.fun/posts/ai-training-sft-dpo-day2/)）。

    Day3 迎来了整个实训的转折点——Agent 智能体实践。用 vLLM 部署 Qwen3-1.7B，通过 `--enable-auto-tool-choice` 和 `--tool-call-parser hermes` 让模型自动选择并解析工具调用。四个任务层层递进：Task1 让 Agent 调用 `safe_calculator` 和 `unit_converter`，模型自动判断表达式需要计算、单位需要转换，你只需要说"做什么"，不需要说"怎么做"。Task2 设计了 MathSkill、SalesDataSkill、ReportSkill 三个模块化 Skill，关键发现是 `system_message` 中"must be called"这种强制性措辞远比温和提示有效——措辞决定工具调用成功率。Task3 对 10 道 GSM8K 题目批量推理，通过为每道题创建独立 Agent 实例实现"fresh start"避免上下文污染，最终 9/10 正确。Task4 实现"检索→提取→回答→计划→报告→验证"闭环，Agent 首次验证失败后自动重写完整报告并再次验证通过——第一次真切感受到 Agent 的"发现错误→自我修正"能力（[Day3 笔记](https://xingwangzhe.fun/posts/ai-training-agent-day3/)）。

    Day4 正式接手 B4 模块，撰写 Proposal 设计文档。核心思路是：针对 Qwen3.5-4B 小模型输出不稳定的问题，设计了"双保险 Prompt"（system message 长格式指令 + user message 末尾短格式提醒）+"三层级联解析"（`json.loads` → `json.JSONDecoder().raw_decode()` 从尾部精准截取 → 搜索 `tool_calls` 标记包装残缺数组）+"AIMessage 互斥约束"。为什么把短格式提醒放在 user message 末尾？因为对于 ChatML 模型，该位置更靠近生成起点，注意力权重更高。解码策略选择贪心解码（`do_sample=false`, `temperature=0`），保证工具调用决策的确定性（[Day4 Proposal](https://xingwangzhe.fun/posts/ai-training-agent-day4-proposal/)）。

    Week2 进入深水区，在 Proposal 基础上完成五项进阶升级。1）多工具并发：Prompt 从"choose exactly one"改为"zero, one, or multiple"，真实 Qwen3.5-4B 成功单轮输出 3 个并发 tool_calls——"不是模型能力不够，而是 prompt 给它的自由度决定了它的行为边界"。2）Plan-and-Execute：双阶段设计，阶段1生成 `reasoning` + `plan` 数组，阶段2按计划逐步执行并汇总，为此新增 5 个专用函数。3）多模型切换：`_MODEL_CACHE` 天然支持多模型 hash 隔离，仅加 10 行 `model_name` resolver 和 `--model_name` CLI 参数——"好架构的特点是：当新需求来临时，改动集中在最薄的接口层"。4）tools_schema 传参对比：A/B 实验中 prompt 注入成功率 83.3%（最终优化至 100%），builtin 内置传参成功率 0%——Qwen3.5-4B 输出的是原生 XML 而非 JSON，三层 JSON 解析器全部失效。这个结果让我深刻体会到："不要迷信'原生能力'，在小模型上，你能掌控的东西才是你真正拥有的东西"。5）批量基准测试：`b4_batch_benchmark.py` 覆盖 6 个场景，自动统计成功率和延迟。一个关键 bug 浮现——计算题模型同时输出 content 和 tool_calls，触发互斥校验失败。修复方案不是强行让模型改，而是加入"content 优先"的静默修复策略——容错比完美更重要（[Week2 笔记](https://xingwangzhe.fun/posts/ai-training-b4-llm-week2/)）。

    最终验收时，基础要求 4 项 + 进阶 5 项全部完成，prompt_json 模式下 6/6 成功率 100%。整个实训以一篇 [验收讲解：从基础到进阶的五维实践](https://xingwangzhe.fun/posts/ai-training-b4-week2-review/) 收尾，从架构回顾到经验教训再到改进方向，形成闭环。

- **遇到的最大挑战**：在 B4 模块开发中，主要遇到三个层面的挑战。其一是**真实模型与 Mock 模式的行为差异**：Mock 下联调全通过，但切换到本地 Qwen3.5-4B 后，模型输出常夹杂解释文本、markdown 代码块包裹的 JSON、CoT 思考标签（`<|thinking|>`）污染 JSON 语法，甚至 `tool_calls` 数组被截断，导致解析大面积失败；其二是**与 B1/B3 联调时的接口不一致**：B1 以 OpenAI 格式传 messages，而 B4 期望的 `tool_calls` 结构与 B3 动态加载 Skill 时需要的 `function.name` 命名风格不统一，初期互相对不上；其三是**tools_schema 传递方式的选择困境**：HuggingFace Transformers 提供了 `apply_chat_template(tools=...)` 内置传参方式，理论上更规范优雅，但实际测试发现 Qwen3.5-4B 输出的是 XML 格式的 `<tool_call>` 标记，与本模块的纯 JSON 解析器完全不兼容。

- **如何克服的**：针对解析问题，我没有强行要求模型"必须输出完美 JSON"，而是查阅了 `transformers` / `apply_chat_template` 的相关文档，并参考团队其他成员的思路，设计了**三层级联容错解析**（`_extract_json_object` → `_parse_json_with_backtick_tail` → `_parse_tool_calls_fragment`）+ **content 优先于 tool_calls 的互斥归一化**，把严格校验改为"静默修复"，使复杂场景从失败转为成功；针对接口问题，与组员约定了统一的字段命名，推动在 `common/schemas.py` 中用 `make_ai_message / validate_ai_message / validate_messages` 对所有模块间数据交换做统一校验，从根源消除字段不一致；针对 tools_schema 选择困境，设计了**严格的 A/B 对比实验**——用相同的 6 个测试用例分别测试 prompt 注入和内置传参两种方式，实验数据清晰地证明 prompt 注入以 100% 的成功率胜出，内置传参因 XML 输出而不兼容，为默认配置提供了数据支撑。

- **心得体会**：

    第一，**小模型工具调用的工程本质是"容错"而非"完美"**。4B 参数的模型无论如何优化 Prompt，都无法像 GPT-4 那样稳定输出 100% 合规的 JSON。从 Day2 的 CoT Prompt 实验（越复杂的提示反而准确率越低）到 Week2 的计算题互斥冲突（模型同时输出 content 和 tool_calls），反复验证了一个事实：对小模型而言，能简单就别复杂。真正有价值的不是追求完美输出，而是设计一套从输入到输出的完整容错链路——通过双保险 Prompt 约束输出方向，通过三层级联解析容忍输出偏差，通过 content 优先策略处理模糊状态。每一层都兜住前一层的漏网之鱼，最终在系统层面实现可接受的可用率。

    第二，**对比实验驱动决策**是 AI 工程的核心方法论。tools_schema 传递方式的选择不是拍脑袋或"看着更规范"，而是通过 6 个测试用例的 A/B 对比数据说话——prompt 注入 100% vs builtin 0%，数据直接推翻了"官方推荐就是最优解"的直觉。Day2 的 β 参数敏感性分析、CoT 模板消融实验，Week2 的批量基准测试，全部遵循同一个原则：不靠直觉，靠数据。这种用数据替代直觉的工程方法，在 AI 系统开发中尤为重要。

    第三，**好架构预埋扩展性，改动能集中在最薄的接口层**。从 Day4 Proposal 的 `_MODEL_CACHE` 到 Week2 的模型切换，仅 10 行代码就实现了 `--model_name` 动态切换。Plan-and-Execute 模式通过新增 5 个独立函数实现，不影响已有 ReAct 流程。好的架构不是预判所有需求，而是让新需求来临时改动最小。

    第四，**持续记录本身就是一种学习方法**。从 Day1 到验收，6 篇博文累计数万字，每一篇都是当天做完、当天写的。写博客的过程迫使我理清思路：不仅要知道"怎么做"，还要能解释"为什么这么做"和"还有没有更好的做法"。Day2 的 DPO 准确率反降就是写博客时复盘发现的——如果不写下来，可能就略过了这个反常结果。这种"做完→写下来→反思→再优化"的正循环，是实训给我最宝贵的习惯。

    整体下来，三周实训在 Python 工程化、Prompt 工程、跨模块协作调试三个维度都有明显提升。从一个连 PyTorch 的 `x @ w` 都要重新捡起来的初学者，到能让 Qwen3.5-4B 自主完成 6 个场景的工具调用决策且成功率 100%，这种从零到一的成长感，是任何教科书都给不了的。


---

**作者：**xingwangzhe

**本文链接：**[https://xingwangzhe.fun/posts/ai-training-b4-final-report/](https://xingwangzhe.fun/posts/ai-training-b4-final-report/)

本文采用[知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-sa/4.0/)进行许可。