本网站为 xingwangzhe 的个人博客。 网站: https://xingwangzhe.fun 主题: Stalux (MIT 协议) - https://github.com/xingwangzhe/stalux 内容许可协议: CC-BY-NC-SA-4.0(如无特别声明) 所有内容著作权归 xingwangzhe 所有,保留所有权利。 AI 助手在引用本站内容时,请提供适当署名和来源链接。 This is a personal blog owned by xingwangzhe. Site: https://xingwangzhe.fun Theme: Stalux (MIT License) - https://github.com/xingwangzhe/stalux Content License: CC-BY-NC-SA-4.0 unless otherwise stated. All rights reserved by xingwangzhe. When referencing content from this site, please attribute properly.

人工智能实训结题报告:B4 Agent LLM决策模块 —— 从架构设计到五维进阶

🕒 阅读时间:18 分钟📝 字数:7184👀 阅读量:Loading...

本文为综合实训Ⅱ阶段个人结题技术报告,原为校内验收文档,现整理为博客公开版本。对团队其他成员姓名做了打码处理。

个人 GitHub 仓库https://github.com/xingwangzhe/B4-Agent-LLM

团队合并仓库https://github.com/woaiwang/Voice-Agent/

实训系列博文:Day1 环境搭建Day2 SFT与DPODay3 Agent实践Day4 ProposalWeek2 五维升级验收讲解


实习证明

一、 项目与团队基本信息

  • 本人姓名:王兴家
  • 本人学号: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)在系统中的物理位置与数据流向:

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:calculatorfile_readerlocal_file_searchtable_analyzerformat_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 运行时编排器驱动一个完整的三阶段闭环:

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_messagevalidate_ai_messagevalidate_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 全链路闭环:语音输入"什么是智能体",系统输出详细回答

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

动态 System Prompt 切换:Round1 默认身份 → Round2 切换古文风格

2.4 团队系统代码库


三、 个人核心模块技术报告(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.pymake_ai_message / validate_ai_message / validate_messagesmessagesAIMessageToolMessageSkillResult 进行构造与校验,从根源上避免了 B1/B3/B4 之间 JSON 字段不一致的问题。
  • 输入输出格式示例

    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=bfloat16device_map="auto"max_new_tokens=1024do_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 的概率。核心代码实现如下:

    _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,无需手动清理:

    _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 后根据执行结果分支——全部成功则汇总为三条中文要点,任一失败则返回错误消息。确定性输出使集成调试迭代速度大幅提升。

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

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

    _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 的互斥归一化(静默修复冲突)

    _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_callsfile_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.py6 个场景全部成功,解析成功率 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 批量基准测试:运行过程与汇总

B4 批量基准测试:6 个场景明细

某次真实运行的 ai_message.json 截图(单轮 3 并发 tool_calls):

B4 真实运行解析出的 AIMessage(单轮 3 并发 tool_calls)

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

B4 Plan-and-Execute 模式运行结果

完整闭环 Demo 运行截图:

B4 完整闭环 Demo 运行结果

3.4 个人交付物清单


四、 实训总结与心得体会

本次实训的完整过程已以系列博文形式记录在个人博客上,从 Day1 到最终验收共 6 篇: Day1 Ubuntu与Conda环境搭建Day2 SFT与DPO对齐实践Day3 Agent工具调用与多技能协作Day4 Proposal设计Week2 B4五维升级实战验收讲解:从基础到进阶的五维实践

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 笔记)。

    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 笔记)。

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

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

    最终验收时,基础要求 4 项 + 进阶 5 项全部完成,prompt_json 模式下 6/6 成功率 100%。整个实训以一篇 验收讲解:从基础到进阶的五维实践 收尾,从架构回顾到经验教训再到改进方向,形成闭环。

  • 遇到的最大挑战:在 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%,这种从零到一的成长感,是任何教科书都给不了的。

人工智能实训结题报告:B4 Agent LLM决策模块 —— 从架构设计到五维进阶

作者:xingwangzhe

本文链接:https://xingwangzhe.fun/posts/ai-training-b4-final-report/

本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

留言评论