本篇是全系列的心脏。看完它,你会明白 Manus、Devin、ChatGPT 的"插件模式",底层都是同一件事。代码对应
zhiliao/agent/core.py和zhiliao/agent/llm.py。
一、先看一遍魔术
在知了里问它一句,观察它的内部:
你 > 现在几点?顺便算下 365*24 等于多少
知了 > (内部实际发生了什么)
1. [思考] 用户问两件事,都需要工具。并行调用:
→ get_current_time()
→ calculator("365*24")
2. [观察] 时间工具返回 "2026-09-30 14:22 星期三"
计算器返回 "365*24 = 8760"
3. [思考] 拿到全部事实,可以回答了
4. [回答] "现在是 2026-09-30 14:22(星期三)。一年有 8760 小时。"
模型本身只会"生成下一段文字",它不能真的执行工具。整个魔术的机关在两个角色身上:
- 模型负责"点菜":返回一个结构化请求——"我要调用 calculator,参数是 365*24"
- Agent 循环负责"炒菜":解析请求 → 真正执行 → 把结果端回给模型
这套机制的大名:Function Calling(函数调用)。
二、Function Calling 协议:一次完整对话长什么样
和模型的所有来往都是 messages 消息列表。带工具的一轮,消息是这样滚起来的:
[1] {"role":"system", "content":"你是知了…可用工具:calculator…"}
[2] {"role":"user", "content":"现在几点?顺便算下 365*24"}
[3] {"role":"assistant","tool_calls":[{"function":{"name":"get_current_time","arguments":"{}"}}]}
[4] {"role":"tool", "tool_call_id":"call_1","content":"2026-09-30 14:22 星期三"}
[5] {"role":"assistant","tool_calls":[{"function":{"name":"calculator","arguments":"{\"expression\":\"365*24\"}"}}]}
[6] {"role":"tool", "tool_call_id":"call_2","content":"365*24 = 8760"}
[7] {"role":"assistant","content":"现在是 2026-09-30 14:22(星期三)…"}
三个协议要点(新手最容易翻车的地方):
- 请求里要带工具清单(
tools参数):每个工具用 JSON Schema 描述名字、功能、参数——模型只认识你写进清单的文字。 - 模型的话必须原样回填:第 [3]、[5] 条 assistant 消息(含 tool_calls)要 append 回消息列表,模型才记得自己"刚说了什么"。
- 工具结果用
role:"tool"回传,且tool_call_id必须和请求对应——这是"菜端上桌"的仪式,少一步模型就懵了。
三、动手实现:先写"大脑接口"
agent/llm.py,核心只有一个方法,我们逐段看:
def chat(self, messages, tools=None, temperature=0.7):
kwargs = dict(model=self.model, messages=messages, temperature=temperature)
if tools:
kwargs["tools"] = tools
kwargs["tool_choice"] = "auto" # 模型自己决定用不用工具
resp = self.client.chat.completions.create(**kwargs)
msg = resp.choices[0].message
result = {"role": "assistant", "content": msg.content or ""}
if getattr(msg, "tool_calls", None):
result["tool_calls"] = [
{"id": tc.id, "type": "function",
"function": {"name": tc.function.name, "arguments": tc.function.arguments}}
for tc in msg.tool_calls
]
return result
注意 arguments 是字符串(JSON 序列化过的),不是对象——协议如此,后面解析它。这一层把各家 API 的响应统一成自己的 dict 格式,上层代码就不用关心"我现在用的是哪家模型"。
四、动手实现:核心循环(全文仅 60 行)
def run(self, user_input: str) -> dict:
# 1) 用户发言进入记忆
self.memory.add("user", user_input)
# 2) 系统提示词(人设+规矩+技能目录)+ 检索到的记忆/知识,注入本轮
system_text = build_system_prompt(...)
context_blocks = [...] # 长期记忆片段、知识库片段
if context_blocks:
system_text += "\n\n" + "\n\n".join(context_blocks)
# 3) 本轮完整消息 = 系统提示 + 会话历史
messages = [{"role": "system", "content": system_text}] + self.memory.view()
# ★★★ 核心循环:思考 → 行动 → 观察 ★★★
steps = []
for _ in range(self.cfg.max_iterations): # 上限 8 次,防失控
assistant_msg = self.llm.chat(messages, tools=self.registry.schemas())
messages.append(assistant_msg) # 模型发言必须回填!
tool_calls = assistant_msg.get("tool_calls")
if not tool_calls:
# 模型不再要工具 → 这就是最终回答
reply = assistant_msg["content"].strip()
self.memory.add("assistant", reply)
self.memory.maybe_summarize(self.llm) # 历史太长就滚动摘要
return {"reply": reply, "steps": steps}
for call in tool_calls: # 可能并行要多个工具
result = self.registry.call(call["function"]["name"],
call["function"]["arguments"])
steps.append({"tool": call["function"]["name"], "result": result[:300]})
# 协议要求:结果以 role="tool" 回传,id 对应
messages.append({"role": "tool",
"tool_call_id": call["id"], "content": result})
# 4) 兜底:循环超限,去掉工具强制模型总结
final = self.llm.chat(messages)
...
对照第一节的魔术流程读一遍,是不是严丝合缝?
五、三个用钱换来的工程经验
经验 1:必须设 max_iterations。
不加循环上限的 Agent 迟早出事:模型可能陷入"调用 A 工具 → 结果不对 → 再调 A"的死循环,而你按 token 付钱。8 次不够的任务,正确解法是把任务拆小,而不是无限循环。
经验 2:工具结果不进长期记忆。
注意上面代码里 memory.add 只在开头(用户输入)和结尾(最终回答)各一次。工具调用的中间过程只活在 messages 这个"本轮工作区"里,任务结束即弃。原因:① 中间结果又长又贵,塞进历史每次请求都要重发一遍;② 用户下一句话通常不需要知道你调了什么工具。这就是"工作记忆"和"长期记忆"的分界线。
经验 3:工具报错不要抛异常,要返回错误文本。
registry.call 把一切异常转成 【工具出错】... 字符串回给模型。因为模型拿到错误描述后常常能自己纠正(换参数、换工具)。一个工具挂了不该掀翻整个 Agent——容错下沉到工具层,韧性长在循环里。
六、测试它
python main.py
你 > 现在几点?3000 美元换成人民币大概多少?(汇率自己查)
你会看到它先查时间、再搜汇率、调用计算器,最后汇成答案——每一步都实时打印(show_steps 函数),这种透明性在产品化时(第 8 篇)会成为 UI 的核心元素。
本篇作业
- 改小
max_iterations(比如 2),问一个需要 3 步的问题,观察兜底逻辑触发。 - 在
registry.call里加一行print,把模型的原始 arguments 打出来,亲眼看看"点菜单"长什么样。 - 思考:为什么
tool_choice设为auto而不是required?(提示:模型该有"不需要工具直接回答"的自由)
下一篇预告:《04 · 工具系统》——循环有了,现在给它装双手:工具注册表的设计、13 个内置工具各自的看点(手写计算器解析器、文件沙箱、SSRF 防护、代码执行),以及 5 分钟给你的 Agent 加一个自定义工具。