本网站为 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 LLM决策模块:验收讲解 —— 从基础到进阶的五维实践

00👀 阅读量:Loading...

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

前文 Week2 五维升级实战 记录了全部开发过程,本文在此基础上按验收要求重新组织,侧重模块定位 → 基础实现 → 模块协作 → 进阶亮点 → 测试结果的讲解逻辑。


一、模块定位

1.1 一句话概括

B4 是 Agent 系统的 LLM 决策模块——整个系统的“大脑”。

它接收对话历史(messages)和工具说明(tools_schema),调用本地部署的 Qwen3.5-4B 模型,输出标准化的 AIMessage,决定“是否需要调用工具、调用哪个工具、传入什么参数”。

1.2 在系统中的位置

B4 属于 Agent 系统(B1-B5)中的决策层,上下关系如下:

flowchart TB
subgraph "Agent 系统架构 (B1-B5)"
direction TB
B5["B5 记忆模块<br/>读取/保存记忆文档"]
B1["B1 Runtime<br/>消息编排 + 循环控制"]
B4["✅ B4 LLM 决策<br/>调用模型生成 AIMessage"]
B3["B3 工具层<br/>生成 schema + 执行工具"]
B2["B2 Skill<br/>具体工具函数"]
end
User["用户输入"] --> B1
B1 --> B5
B5 --> B1
B1 --> B4
B4 --> B1
B1 --> B3
B3 --> B2
B2 --> B3
B3 --> B1
B1 --> Output["最终回答"]
style B4 fill:#4CAF50,color:#fff,stroke:#333

1.3 核心数据流

B1 Runtime → 调用 B4.generate_ai_message(messages, tools_schema)
→ B4 加载模型 → 注入 messages + schema → LLM 推理
→ 解析原始输出 → 返回 AIMessage {content, tool_calls}
→ B1 判断是否有 tool_calls
→ 有:B3 执行 → ToolMessage → B1 追加消息 → 再次调 B4
→ 无:输出 final_answer

1.4 完成概况

类型 完成情况
基础要求 4 项 ✅ 全部完成
进阶要求 5 项 ✅ 全部完成
独立运行演示 ✅ 5 种运行模式
团队系统集成 ✅ 标准接口 generate_ai_message()

二、基础功能实现

2.1 模型加载

model.yaml 读取配置,使用 transformers 加载 Qwen3.5-4B:

configs/model.yaml
model:
model_name_or_path: /root/assignment_B/Qwen3.5-4B
torch_dtype: bfloat16
device_map: auto
generation:
max_new_tokens: 1024
temperature: 0
# _load_model_config() 解析 YAML
# _load_model_bundle() 加载 tokenizer + model
config = read_yaml(model_config_path)
tokenizer = AutoTokenizer.from_pretrained(**config)
model = AutoModelForCausalLM.from_pretrained(**config)

2.2 tools_schema 绑定

B3 生成的工具说明通过 prompt 注入 方式嵌入系统消息:

# _build_prompt_messages() 将 tools_schema JSON 注入 prompt
system_prompt = read_prompt("local_tool_agent.txt")
system_prompt += "\n\nAvailable tools:\n" + json.dumps(tools_schema)
messages = [{"role": "system", "content": system_prompt}] + user_messages

2.3 AIMessage 生成与解析

模型原始输出是文本字符串,经过三层容错解析:

flowchart TB
A["模型原始输出<br/>(文本字符串)"] --> B["json.loads()<br/>标准JSON解析"]
B --> C{"解析成功?"}
C -->|| D["_candidate_to_message()<br/>→ 标准 AIMessage"]
C -->|| E["_extract_json_object()<br/>智能提取 JSON"]
E --> F{"提取成功?"}
F -->|| D
F -->|| G["_parse_json_with_backtick_tail()<br/>_parse_tool_calls_fragment()<br/>容错回退"]
G --> D

2.4 输出记录

每个运行案例保存两份 JSON 文件:

文件 内容
raw_model_output.json 模型原始 decode 文本
ai_message.json 标准化 AIMessage

ai_message.json 输出示例(有工具调用时):

{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_001",
"name": "file_reader",
"args": { "path": "docs/agent_intro.txt", "max_chars": 2000 }
},
{
"id": "call_002",
"name": "calculator",
"args": { "expression": "3.14 * 5" }
}
]
}

2.5 基本演示命令

Terminal window
cd code
# 无工具调用:模型直接回答
python b4_local_agent_llm.py \
--model_config ../configs/model.yaml \
--messages ../data/messages/messages_no_tool.json \
--tools_schema ../data/messages/tools_schema_basic.json \
--mode prompt_json --outdir ../outputs/B4_llm/no_tool_demo
# 有工具调用:生成 tool_calls
python b4_local_agent_llm.py \
--model_config ../configs/model.yaml \
--messages ../data/messages/messages_with_tool.json \
--tools_schema ../data/messages/tools_schema_basic.json \
--mode prompt_json --outdir ../outputs/B4_llm/with_tool_demo

三、与其他模块的交互

B4 在 Agent Loop 中承担“一次决策”的角色。

3.1 Agent Loop 完整流程

sequenceDiagram
participant User as 用户
participant B1 as B1 Runtime
participant B5 as B5 记忆
participant B4 as B4 LLM
participant B3 as B3 工具层
participant B2 as B2 Skill
User->>B1: 提交问题
B1->>B5: 查找记忆
B5-->>B1: 返回 global/selected memory
B1->>B4: generate_ai_message(messages, tools_schema)
B4->>B4: 加载模型 → LLM 推理 → 解析 AIMessage
B4-->>B1: 返回 AIMessage {content, tool_calls}
B1->>B1: 判断是否有 tool_calls?
alt 有 tool_calls
B1->>B3: 执行 tool_calls
B3->>B2: 调用对应 Skill
B2-->>B3: 返回工具结果
B3-->>B1: 返回 ToolMessage
B1->>B1: 追加 AI + Tool 到 messages
B1->>B4: 再次 generate_ai_message(完整 messages)
B4-->>B1: 新 AIMessage
B1->>B1: 再次判断...(循环直到无 tool_calls)
else 无 tool_calls
B1-->>User: 输出 final_answer
end
B1->>B5: 保存记忆(可选)

3.2 接口定义

B4 对外暴露的唯一接口:

def generate_ai_message(
model_config: str, # model.yaml 路径
messages: list[dict], # 消息序列
tools_schema: list[dict],# 工具说明
mode: str = "prompt_json",
model_name: str = None,
tool_calling: str = None,
) -> dict: # 标准 AIMessage

3.3 B4 不做什么

职责 说明
❌ 不执行工具 由 B3 调用 B2 Skill
❌ 不管理消息循环 由 B1 控制 max_turns 和状态
❌ 不读写记忆 由 B5 负责
✅ 只做一件事 messages + tools_schema → LLM → AIMessage

四、进阶功能详解

五项进阶要求全部完成,逐一说明。


4.1 进阶一:单轮多 tool_calls 与多 ToolMessage

为什么需要这个功能?

基础版本 prompt 写着 "Choose exactly one tool",模型每轮只能生成一个 tool_call。但真实任务往往需要同时调用多个工具——比如“读取文件 同时 计算一个表达式 同时 搜索相关内容”。

改动本质:3 行 prompt 的变化

项目 改前 改后
prompt 约束 "Choose exactly one schema" "You may include zero, one, or multiple tool_calls"
示例展示 1 个 tool_call 示例含 2 个 tool_call
尾部处理 只读最后 1 条 ToolMessage 遍历所有 ToolMessage
关键源码改动 1:Prompt 模板(_build_prompt_messages()
"Valid schema B:\n"
"Valid schema B (call one or more tools, content must be empty):\n"
'{"content":"","tool_calls":[{"id":"call_001","name":"file_reader",'
'"args":{"path":"docs/agent_intro.txt","max_chars":2000}}]}\n\n'
'"args":{"path":"docs/agent_intro.txt","max_chars":2000}},{"id":"call_002",'
'"name":"calculator","args":{"expression":"2+2"}}]}\n\n'
...
"Choose exactly one schema: final content with an empty tool_calls array, or empty content with tool calls. "
"You may include zero, one, or multiple tool_calls in the array. "
关键源码改动 2:Mock 生成器(_mock_generate())— 遍历所有 ToolMessage
def _mock_generate(messages: list[dict]) -> dict:
tool_messages = [m for m in messages if m.get("role") == "tool"]
if not tool_messages:
# 多个 tool_calls 演示
return make_ai_message("", [
{"id": "call_001", "name": "file_reader",
"args": {"path": "docs/agent_intro.txt", "max_chars": 2000}},
{"id": "call_002", "name": "local_file_search",
"args": {"query": "Agent"}},
])
latest = tool_messages[-1]
result = _extract_tool_result(latest)
if latest.get("status") != "success" ...
return make_ai_message(f"工具调用失败,无法完成请求:{detail}", [])
output = result.get("output") or {}
content = output.get("content") if isinstance(output, dict) else None
...
answer = "三条中文要点如下:\n" + ...
return make_ai_message(answer, [])
# 遍历所有 ToolMessage,逐个检查状态
for tm in tool_messages:
if tm.get("status") != "success":
try:
result = _extract_tool_result(tm)
if result.get("status") != "success":
...
return make_ai_message(f"工具调用失败,无法完成请求:{detail}", [])
except ValueError:
return make_ai_message(f"工具调用失败:无法解析工具返回内容", [])
# 全部成功则汇总所有 ToolMessage 的结果
summaries = []
for tm in tool_messages:
try:
result = _extract_tool_result(tm)
output = result.get("output") or {}
content = output.get("content") if isinstance(output, dict) else None
if isinstance(content, str) and content.strip():
summaries.append(content)
except ValueError:
pass
combined = "\n".join(summaries) if summaries else "工具结果未提供可提取内容"
points = _three_points(combined)
answer = "三条中文要点如下:\n" + points
return make_ai_message(answer, [])

改动前只取 tool_messages[-1] 最后一条工具结果,改动后 for tm in tool_messages 遍历所有。

验证结果

flowchart TB
subgraph "多 tool_calls 生成"
A["messages<br/>(用户问题)"] --> B["B4 LLM"]
B --> C["tool_calls[0]: file_reader"]
B --> D["tool_calls[1]: calculator"]
B --> E["tool_calls[2]: local_file_search"]
end
subgraph "多 ToolMessage 接收"
C --> F["ToolMessage[0]: 文件内容"]
D --> G["ToolMessage[1]: 计算结果"]
E --> H["ToolMessage[2]: 搜索匹配"]
F --> I["B4 LLM<br/>(再次调用)"]
G --> I
H --> I
I --> J["最终回答:<br/>合并 3 个工具结果"]
end

真实模型测试结果(Qwen3.5-4B):

并发数 场景 状态
2 路 file_reader + calculator ✅ 生成 + 合并回答
3 路 file_reader + calculator + local_file_search ✅ 全部执行成功
5 路 file_reader + calculator + local_file_search + table_analyzer + format_converter ✅ Mock 通过

命令示例

Terminal window
cd code
python b4_local_agent_llm.py \
--model_config ../configs/model.yaml \
--messages ../data/messages/messages_multi_tool.json \
--tools_schema ../data/messages/tools_schema_basic.json \
--mode mock --outdir ../outputs/B4_llm/live_demo

输出 ai_message.json

{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_001",
"name": "file_reader",
"args": { "path": "docs/agent_intro.txt", "max_chars": 2000 }
},
{ "id": "call_002", "name": "local_file_search", "args": { "query": "Agent" } }
]
}

4.2 进阶二:Plan-and-Execute 模式

ReAct vs PlanEx

flowchart TB
subgraph "ReAct 模式基础)"
direction TB
R1["用户问题"] --> R2["LLM 决策"]
R2 --> R3["Tool 1"]
R3 --> R4["LLM 决策"]
R4 --> R5["Tool 2"]
R5 --> R6["LLM 决策"]
R6 --> R7["最终回答"]
end
subgraph "Plan-and-Execute进阶)"
direction TB
P1["用户问题"] --> P2["LLM 生成计划"]
P2 --> P3["Plan: 3 步计划"]
P3 --> P4["Step 1 → Tool 1"]
P4 --> P5["Step 2 → Tool 2"]
P5 --> P6["Step 3 → Tool 3"]
P6 --> P7["最终回答"]
end

双阶段流程

阶段 1 — Plan 生成:模型输出结构化计划,含 reasoning 和 plan 数组

真实模型生成的 2 步计划:

{
"reasoning": "用户需要总结文档要点,同时了解搜索功能,最后计算示例。",
"plan": [
{
"step": 1,
"description": "读取 Agent 介绍文档",
"tool_call": {
"name": "file_reader",
"args": { "path": "docs/agent_intro.txt", "max_chars": 2000 }
}
},
{
"step": 2,
"description": "执行示例数学计算",
"tool_call": {
"name": "calculator",
"args": { "expression": "3.14 * 5" }
}
}
]
}

阶段 2 — Execute:逐一执行计划步骤,全部完成后汇总结果

关键源码改动:generate_ai_message() 新增 plan_execute 分支
elif mode == "plan_execute":
tool_messages = [m for m in messages if m.get("role") == "tool"]
backend_plan = config.get("model", {}).get("backend", "transformers")
if backend_plan == "mock" or not _has_torch():
if not tool_messages:
# 阶段 1: 生成计划(Mock 返回 3 步固定计划)
ai_message = _mock_plan_execute(messages)
...
parsed_candidate = {
"content": "",
"tool_calls": ai_message["tool_calls"],
"plan_step_mode": "plan",
"reasoning": "Mock plan.",
"total_steps": len(ai_message["tool_calls"]),
}
status = "success"
else:
# 阶段 2: 步骤执行(Mock 合并工具结果)
ai_message = _mock_generate(messages)
...
parsed_candidate = {
"content": ai_message["content"],
"tool_calls": [],
"plan_step_mode": "final",
}
status = "success"
else:
raise ValueError("mode must be mock or prompt_json")
# 真实模型:plan → step 双阶段
if not tool_messages:
plan_messages = _build_plan_prompt_messages(messages, tools_schema)
raw_text = _prompt_json_generate(
config_path, config, plan_messages, tools_schema
)
parsed_candidate, ai_message = _parse_plan_output(raw_text)
status = "success"
else:
step_messages = _build_plan_step_prompt_messages(messages, tools_schema)
raw_text = _prompt_json_generate(
config_path, config, step_messages, tools_schema
)
parsed_candidate, ai_message = _parse_model_output(raw_text)
status = "success"

配套新增 5 个 Plan-and-Execute 专用函数:

函数 用途
_build_plan_prompt_messages() 构建计划生成 prompt,引导输出 {"reasoning":"...","plan":[...]}
_build_plan_step_prompt_messages() 步骤执行阶段 prompt,提示“继续下一步 or 最终回答”
_parse_plan_output() 解析计划 JSON,支持 3 种格式
_mock_plan_execute() Mock 模式生成 3 步计划
_has_torch() 检测 torch 是否可用,自动回退 mock

验证结果

场景 Mock 真实模型 状态
计划生成 3 步计划 2 步计划 + reasoning
步骤执行 合并结果 “已完成任务: 2 个文件, 3 条要点”

命令示例

Terminal window
# 计划生成(Mock 模式输出 3 步计划)
cd code
python b4_local_agent_llm.py --model_config ../configs/model.yaml --messages ../data/messages/messages_plan_input.json --tools_schema ../data/messages/tools_schema_basic.json --mode plan_execute --outdir ../outputs/B4_llm/plan_live

输出 3 步计划(Mock 模式):

{
"role": "assistant",
"content": "",
"tool_calls": [
{ "name": "file_reader", "args": { "path": "docs/agent_intro.txt" } },
{ "name": "local_file_search", "args": { "query": "Agent", "root_dir": "docs" } },
{ "name": "calculator", "args": { "expression": "2 + 2" } }
]
}
Terminal window
# 步骤执行(接收工具结果后生成最终回答)
python b4_local_agent_llm.py --model_config ../configs/model.yaml --messages ../data/messages/messages_plan_with_results.json --tools_schema ../data/messages/tools_schema_basic.json --mode plan_execute --outdir ../outputs/B4_llm/plan_exec_live

输出最终回答:

{
"role": "assistant",
"content": "三条中文要点如下:\n1. Agent 系统通常由模型、工具、记忆和执行循环组成\n2. 工具调用让模型能够读取本地文件、执行计算\n3. Memory 为 Agent 提供全局知识和历史对话上下文",
"tool_calls": []
}

4.3 进阶三:多模型切换

实现方式

model.yaml 中定义命名 profiles,通过 --model_name 参数选择:

configs/model.yaml
models:
qwen-4b:
display_name: Qwen3.5-4B (standard)
torch_dtype: bfloat16
device_map: auto
qwen-4b-fast:
display_name: Qwen3.5-4B (fast mode)
torch_dtype: float16
max_new_tokens: 512
关键源码改动:_load_model_config() 新增 model_name 参数
def _load_model_config(model_config: str | Path) -> tuple[Path, dict]:
def _load_model_config(
model_config: str | Path, model_name: str | None = None
) -> tuple[Path, dict]:
path = Path(model_config).resolve()
config = read_yaml(path)
if not isinstance(config, dict):
raise ValueError("model.yaml must contain an object")
if model_name:
models_section = config.get("models", {})
if not isinstance(models_section, dict):
raise ValueError("model.yaml 'models' section must be an object")
if model_name not in models_section:
available = ", ".join(models_section.keys())
raise ValueError(
f"unknown model_name '{model_name}'. Available: {available}"
)
selected = deepcopy(models_section[model_name])
config["model"] = selected
config["model"]["_selected_model_name"] = model_name
print(
f"model: {selected.get('display_name', model_name)}",
file=sys.stderr, flush=True,
)
return path, config

同时 generate_ai_message() 和 CLI parser 新增 model_name 参数透传:

def generate_ai_message(
model_config: str,
messages: list[dict],
tools_schema: list[dict],
mode: str = "prompt_json",
model_name: str | None = None,
) -> dict:
config_path, config = _load_model_config(model_config)
config_path, config = _load_model_config(model_config, model_name)
def build_parser() -> argparse.ArgumentParser:
...
parser.add_argument(
"--model_name", default=None,
help="Select named model profile from model.yaml (models section)",
)

共计约 20 行代码,无侵入式设计——不指定 --model_name 时行为完全不变。

验证结果

测试 命令 输出
默认 不指定 --model_name 使用 model: 默认配置
standard --model_name qwen-4b model: Qwen3.5-4B (standard)
fast --model_name qwen-4b-fast model: Qwen3.5-4B (fast mode)

命令示例

Terminal window
cd code
python b4_local_agent_llm.py \
--model_config ../configs/model.yaml \
--model_name qwen-4b \
--messages ../data/messages/messages_no_tool.json \
--tools_schema ../data/messages/tools_schema_basic.json \
--mode mock --outdir ../outputs/B4_llm/switch_live

控制台输出 model: Qwen3.5-4B (standard),Mock 模式不加载模型,仅验证模型名称配置正确。

Terminal window
# 切换 fast 模式
python b4_local_agent_llm.py \
--model_config ../configs/model.yaml \
--model_name qwen-4b-fast \
--messages ../data/messages/messages_no_tool.json \
--tools_schema ../data/messages/tools_schema_basic.json \
--mode mock --outdir ../outputs/B4_llm/switch_fast_live

4.4 进阶四:tools_schema 传参方式对比

支持两种传参方式:prompt 注入(默认,JSON 格式输出)和 内置传参(通过 apply_chat_template 传入)。实测 prompt 注入方式在当前架构下更可靠,作为默认方案。

关键源码改动:_prompt_json_generate() 新增 tool_calling_mode 参数
def _prompt_json_generate(
config_path: Path,
config: dict,
messages: list[dict],
tools_schema: list[dict],
tool_calling_mode: str = "prompt_json",
) -> str:
...
# 加载模型...
prompt_messages = _build_prompt_messages(messages, tools_schema)
inputs = tokenizer.apply_chat_template(
prompt_messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt", return_dict=True,
)
tool_calling_mode = config.get("tool_calling", {}).get("mode", "prompt_json")
if tool_calling_mode == "builtin":
# 内置传参:不注入 prompt,通过 chat template 的 tools= 参数传入
prompt_messages = deepcopy(messages)
inputs = tokenizer.apply_chat_template(
prompt_messages,
tools=tools_schema, # ← 关键区别
tokenize=True,
add_generation_prompt=True,
return_tensors="pt", return_dict=True,
)
else:
# prompt 注入:tools_schema 拼接到 system message 文本中
prompt_messages = _build_prompt_messages(messages, tools_schema)
inputs = tokenizer.apply_chat_template(
prompt_messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt", return_dict=True,
)
对比维度 prompt 注入 内置传参(builtin)
模型输出格式 JSON {"content":"","tool_calls":[...]} XML <tool_call>
解析结果 ✅ 成功率 83.3% ❌ 0%(JSON 解析器不兼容)
Token 开销 ~500 token 用于 schema 0 token
当前适用 ✅ 默认方案 需适配 XML 解析后可用

命令示例

Terminal window
# prompt 注入方式
cd code
python b4_local_agent_llm.py \
--model_config ../configs/model.yaml \
--messages ../data/messages/messages_with_tool.json \
--tools_schema ../data/messages/tools_schema_basic.json \
--tool_calling prompt_json \
--mode mock --outdir ../outputs/B4_llm/schema_prompt
# 内置传参方式
python b4_local_agent_llm.py \
--model_config ../configs/model.yaml \
--messages ../data/messages/messages_with_tool.json \
--tools_schema ../data/messages/tools_schema_basic.json \
--tool_calling builtin \
--mode mock --outdir ../outputs/B4_llm/schema_builtin

4.5 进阶五:批量基准测试与统计

测试框架

b4_batch_benchmark.py 自动遍历 bench_cases/ 目录,逐一调用 generate_ai_message() 并统计结果。

核心源码:run_benchmark() 框架
from b4_local_agent_llm import generate_ai_message
def run_benchmark(model_config, cases_dir, tools_schema, mode, ...) -> dict:
cases_path = Path(cases_dir).resolve()
case_files = sorted(cases_path.glob("*.json"))
tools = read_json(resolve_cli_path(tools_schema))
results = []
total_success = 0
total_error = 0
for idx, case_file in enumerate(case_files):
messages = read_json(case_file)
case_name = case_file.stem
start = time.perf_counter()
result = generate_ai_message(
model_config, messages, tools, mode,
artifact_dir=outdir,
artifact_stem=f"bench_{case_name}" if outdir else None,
model_name=model_name,
tool_calling=tool_calling,
)
elapsed_ms = (time.perf_counter() - start) * 1000
record = {
"case": case_name,
"status": result["status"],
"latency_ms": round(elapsed_ms, 1),
"tool_calls_count": len(
result["ai_message"].get("tool_calls", [])
),
"content_len": len(result["ai_message"].get("content", "")),
}
if result["status"] == "success":
total_success += 1
else:
total_error += 1
record["error"] = result.get("error", {})
results.append(record)
return {
"total_cases": len(case_files),
"success": total_success,
"error": total_error,
"success_rate": f"{total_success}/{total} ({...}%)",
"avg_latency_ms": round(avg_latency, 1),
"cases": results,
}

命令示例

Terminal window
cd code
python b4_batch_benchmark.py \
--model_config ../configs/model.yaml \
--cases_dir ../data/bench_cases \
--tools_schema ../data/messages/tools_schema_basic.json \
--mode mock --outdir ../outputs/B4_llm/bench_live

Mock 模式下 6 个场景全部通过,成功率 100%。

6 个测试场景

场景 预期行为 输入特点
case_file_read 调用 file_reader 读取本地文档
case_calculator 调用 calculator 数学表达式 3.14 * 5 - 2.5
case_file_search 调用 local_file_search 关键词搜索
case_multi_tool 并发 3 个工具 file_reader + calculator + local_file_search
case_direct_answer 直接回答,不调工具 “法国的首都是哪里?”
case_table_analyzer 调用 table_analyzer 分析 CSV

prompt_json 逐样例统计

场景 状态 tool_calls 延迟 说明
case_file_read ✅ success 1 1574.5 ms 正确调用 file_reader
case_calculator ❌ error 0 9222.5 ms content 和 tool_calls 同时非空
case_file_search ✅ success 1 3592.3 ms 正确调用 local_file_search
case_multi_tool ✅ success 3 7361.7 ms 3 路并发全部成功
case_direct_answer ✅ success 0 769.7 ms 直接回答,不调工具
case_table_analyzer ✅ success 1 1724.8 ms 正确调用 table_analyzer

各场景延迟统计

xychart-beta
title "各测试场景延迟对比(ms)"
x-axis ["file_read", "calculator", "file_search", "multi_tool", "direct_answer", "table_analyzer"]
y-axis "延迟 (ms)" 0 --> 10000
bar [1574.5, 9222.5, 3592.3, 7361.7, 769.7, 1724.8]

各场景 tool_calls 数量

xychart-beta
title "各场景工具调用次数"
x-axis ["file_read", "calculator", "file_search", "multi_tool", "direct_answer", "table_analyzer"]
y-axis "tool_calls 数" 0 --> 4
bar [1, 0, 1, 3, 0, 1]

传参方式对比

xychart-beta
title "两种传参方式成功率对比"
x-axis ["prompt_json", "builtin"]
y-axis "成功率 (%)" 0 --> 100
bar [83.3, 0]

失败的 calculator 用例 — 已修复

唯一失败的原因是模型同时输出了 contenttool_calls,违反互斥约束。

修复 1:_candidate_to_message() — content 优先于 tool_calls
def _candidate_to_message(candidate: dict) -> tuple[dict, dict]:
...
content = candidate.get("content", "")
tool_calls = candidate.get("tool_calls", [])
# 规范化:如果两者都非空,优先使用 content,清空 tool_calls
if content and tool_calls:
print(
"⚠️ 警告:模型同时提供了 content 和 tool_calls,已忽略 tool_calls",
file=sys.stderr, flush=True
)
tool_calls = [] # 清空,使最终回答优先
message = {
"role": "assistant",
"content": candidate.get("content", ""),
"tool_calls": candidate.get("tool_calls", []),
"content": content,
"tool_calls": tool_calls,
}
validate_ai_message(message)
has_content = bool(message["content"].strip())
has_tool_calls = bool(message["tool_calls"])
if has_content == has_tool_calls:
raise ValueError(
"model output must contain either final content or tool calls, but not both"
)
# 移除互斥检查(因为已经规范化)

改动要点:移除互斥 raise → 改为静默修复,记录一条 stderr 警告。模型偶尔“说人话的同时还想调工具”时,优先输出内容。

修复 2:新增 _extract_json_object() 容错解析
def _extract_json_object(text: str) -> dict | None:
"""从文本中依次尝试每个 '{' 位置,直到成功解析出一个 JSON 对象。"""
decoder = json.JSONDecoder()
pos = 0
while True:
start = text.find('{', pos)
if start == -1:
return None
try:
obj, end = decoder.raw_decode(text[start:])
return obj
except json.JSONDecodeError:
pos = start + 1
continue
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 # 不符合要求则继续其他容错
# 原有容错逻辑
try:
candidate = _parse_json_with_backtick_tail(raw_text, exc)

改动要点:在现有两层容错(backtick + fragment)之前,增加 _extract_json_object——逐位置尝试解析 {,应对模型输出中夹杂前缀文本的情形。


五、完成情况总览

5.1 基础要求

序号 要求 状态 对应实现
1 读取 model.yaml 加载本地模型 _load_model_config()
2 接收 tools_schema 完成工具绑定 _build_prompt_messages()
3 解析模型输出为标准 AIMessage _candidate_to_message()
4 JSON 格式记录原始输出与 AIMessage save_raw_output / save_ai_message

5.2 进阶要求

序号 要求 状态 亮点
1 单轮多 tool_calls + 多 ToolMessage 3 行 prompt 改变,支持 3 路并发
2 Plan-and-Execute 模式 双阶段:Plan → Execute
3 多模型切换 10 行代码,--model_name 参数
4 tools_schema 传参对比 83.3% vs 0%
5 批量测试 + 成功率统计 6 场景,成功率 83.3%

5.3 GitHub 仓库

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

包含完整代码、配置、测试数据以及 22 个场景的运行结果。


参考

参考 链接
ReAct https://arxiv.org/abs/2210.03629
HuggingGPT https://arxiv.org/abs/2303.17580
ToolLLM https://arxiv.org/abs/2307.16789
Week2 实战记录 https://xingwangzhe.fun/posts/ai-training-b4-llm-week2/

B4 LLM决策模块:验收讲解 —— 从基础到进阶的五维实践

作者:xingwangzhe

本文链接:https://xingwangzhe.fun/posts/ai-training-b4-week2-review/

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

留言评论