人工智能实训结题报告:B4 Agent LLM决策模块 —— 从架构设计到五维进阶
本文为综合实训Ⅱ阶段个人结题技术报告,原为校内验收文档,现整理为博客公开版本。对团队其他成员姓名做了打码处理。
个人 GitHub 仓库:https://github.com/xingwangzhe/B4-Agent-LLM
团队合并仓库:https://github.com/woaiwang/Voice-Agent/
实训系列博文:Day1 环境搭建 → Day2 SFT与DPO → Day3 Agent实践 → Day4 Proposal → Week2 五维升级 → 验收讲解
一、 项目与团队基本信息
- 本人姓名:王兴家
- 本人学号: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: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 运行时编排器驱动一个完整的三阶段闭环:
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(对话记录)
详细执行流程:
-
初始化阶段:B1读取
runtime_input.json,加载系统提示词。调用B5检索历史记忆并注入到messages中。调用B3读取tools.yaml生成tools_schema.json(OpenAI function calling格式)。 -
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实时写入磁盘,支持断点恢复。
- 决策步(Stage 1):当messages末尾为user或tool时,B1调用B4的
-
收尾阶段:生成最终答案
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消费 |
集成过程中遇到的问题与解决
在模块合并联调时,主要遇到了以下问题:
-
JSON字段不一致:初始联调时B1以OpenAI格式传递messages,但部分字段名和嵌套结构与B4/B3预期不匹配。最终通过统一定义
common/schemas.py中的make_ai_message、validate_ai_message、validate_messages三个核心函数,对所有模块间的数据交换进行严格校验,解决了此问题。 -
B4与B3数据格式分歧:B4输出的tool_calls中工具名称使用"name"字段,但B3在动态加载Skill时需要"function.name"格式。通过在B3侧增加字段归一化层,自动兼容两种命名风格。
-
Mock模式与真实模型的行为差异:在Mock模式下联调通过的全部场景,切换到真实Qwen3.5-4B模型后出现了模型输出解析失败的情况。最终通过增强B4的JSON解析从严格互斥改为静默修复后,切换回真实模型时部分场景从失败变为成功。
-
B1断点恢复的"坏消息回滚"机制:联调中发现当LLM解析失败时,messages中残留的无效assistant消息会导致后续重试失败。B1实现了自动检测与回滚——若trace状态为
llm_parse_error且末尾角色为assistant,则自动弹出该消息并重试。
2.3 最终产品展示 (Demo)
下图展示了 Voice Agent 全链路闭环:用户通过语音输入"什么是智能体?",系统经过语音识别 → Agent 运行时编排 → LLM 决策 → 工具调用,最终输出关于智能体定义、特征和应用的详细回答,完整覆盖 B1→B4→B3→B2→B5 全模块链路:
下图展示了动态 System Prompt 切换功能:Round 1 使用默认身份计算 16+16,Round 2 动态切换为"古文风格"身份后追问"刚才的结果是多少?",模型以"十加十得二十,此乃算术之常理"作答,验证了系统运行时的动态上下文切换能力:
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 字段不一致的问题。
- 上游(输入):由 B1 运行时编排器调用
-
输入输出格式示例:
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 的概率。核心代码实现如下:_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_instructionif prompt_messages and prompt_messages[0].get("role") == "system":prompt_messages[0]["content"] += system_instructionelse:prompt_messages.insert(0, {"role": "system", "content": system_instruction.strip()})# 第二保险:追加到最后一个 User Messageenvelope_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_reminderbreak# ToolMessage 特殊处理:自动追加引导 promptif 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()的级联兜底:json.loads严格解析(最快路径);_extract_json_object逐字符扫描,定位第一个合法 JSON 对象;_parse_json_with_backtick_tail处理尾随`的情况;_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 hittokenizer = 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_callsif 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_tool3 个工具并发调用 3 个并发 tool_calls case_table_analyzer调用 table_analyzer 分析 CSV 单 tool_call 效果:一键运行 6 场景自动化测试,成功率和延迟数据精确可复现。
- 从
3.3 最终结果与性能评估
测试方法
采用三种测试方法相结合的策略:
- 场景化测试:设计 3 个核心场景(初始 tool_call 生成、基于 ToolMessage 的最终回答、工具调用失败处理),分别用真实模型和 Mock 模式验证。
- 批量基准测试:
b4_batch_benchmark.py遍历bench_cases/下 6 个测试用例,覆盖正常、异常、边界、并发等场景,自动统计成功率和平均延迟。 - 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):
某次真实运行的 ai_message.json 截图(单轮 3 并发 tool_calls):
进阶功能② Plan-and-Execute 模式运行截图:
完整闭环 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_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 笔记)。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)。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_nameresolver 和--model_nameCLI 参数——"好架构的特点是:当新需求来临时,改动集中在最薄的接口层"。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 国际许可协议进行许可。








留言评论