Lab 00 · 最简 Agent Loop(对应 Part 1 心智模型)

墨叔:老哥,这是实战的”第零课”。别小看它——Claude Code 再花哨,内核也就是这个循环套了几层壳。你先把这一圈跑通、看明白,后面所有花样才有落脚点。


这个 lab 在练什么

把”Agent = 大厨 + 递纸条的人”这句话落成代码:

  • 大厨 = llm(模型,负责”想”)
  • 递纸条的人 = run_loop(你写的循环,负责”把模型的意图落地、再把结果递回去”)
  • 厨房 = get_weather(工具,真正干活的代码)

跑起来你会看到三行关键日志:[think] 模型要求调用工具 → [act] 工具结果 → [think] 模型说。这就是一次完整的 observe → think → act。


怎么跑

# 默认 mock:不需要任何 API key,离线即可看完整流程
python main.py
 
# 接真实模型(需先有 OPENAI_API_KEY)
export OPENAI_API_KEY=sk-xxx
AGENT_MODE=real python main.py

环境要求:Python 3.10+。本 lab 除标准库外无任何第三方依赖。


你该看懂的几点

  1. 模型不直接执行代码。 它只输出”我想调 get_weather,参数是 {city: 北京}“。真正跑函数的是 run_loop 里的 tool.run()。这一层隔离,是后面所有安全机制的物理基础。
  2. 循环靠”工具调用”驱动。 模型一旦要求调工具,循环就 act;模型不再要求调工具(返回纯文本),循环就 break。终止条件就这么简单。
  3. mock 不是”假货”,是”确定性替身”。 mock_responder 按步数返回预设回复,让你在没有 key 时也能完整观察循环结构。真实模式下,同一个 run_loop 换上 RealLLM 即可,业务代码一行不用改——这就是”面向接口编程”的甜头。

思考题

【思考题】

  1. 如果把 max_steps 设成 1,会发生什么?为什么?
  2. mock 剧本里,第 2 步的 responder 其实”看不到”工具结果也能返回正确答案。真实场景里,模型靠什么知道工具返回了什么?(提示:看 common/runner.py 里 tool 结果被加进了哪条消息)
  3. 这个循环目前是”同步阻塞”的——模型想一步、工具跑一步。你觉得 Claude Code 的”一边想一边显示”是怎么在这个骨架之上加出来的?(呼应 Part 5 流式)

【参考答案】

  1. max_steps=1 时,循环只走一步就结束了。如果这唯一一步模型还在要求调工具(mock 的 step==1 正是这样),循环会因没拿到”最终文本回答”而在循环外补一句”(达到最大步数仍未给出最终回答)“。所以 max_steps 是安全阀,防止模型陷入”调工具→结果→又调工具”的死循环把上下文撑爆。
  2. 真实模型靠的是 run_loop 把工具结果作为一条 role="tool" 的消息追加进 messages。下一轮 llm.chat(messages, tools) 时,模型能看到”我刚才调了 get_weather,返回了’晴,22℃’“,才能据此总结。mock 是我们人为跳过了这一步(直接写死答案),但真实链路里这条 tool 消息是模型感知世界的唯一窗口。
  3. “一边想一边显示”本质是在 run_loop 的每一步 on_step 回调里做增量渲染:模型每吐一个 token 就推到前端,工具每出一段结果就追加显示。骨架不变,只是在”think”阶段从”等整句说完再显示”改成”流式吐字”,在”act”阶段把工具输出实时打印。Part 5 会展开。

下一步

跑通后,去 lab01 看工具怎么从”一个 stub”长成”有 schema、有错误处理、有安全边界”的真正工具集。