S1:Agent 主循环与工具扩展-构建最小智能体
本篇合并 s01、s02 两章。
s01:Agent Loop(主循环)
核心模式
最小智能体 = 一个循环:LLM 不断调用工具,直到自己判断任务完成。
用户提问 → LLM → 需要调用工具? → 执行工具 → 结果回传 → LLM → … → 任务完成,结束
- 用户提问:把问题作为首条
user消息放进messages。 - LLM 决策:每轮把包含完整历史的
messages传给模型,模型自己决定"再调一次工具"还是"直接回答"。 - 结果回传:工具输出作为
tool_result消息追加回messages,成为下一轮的上下文。 - 结束条件:模型不再请求
tool_use时,循环退出。
代码实现
def agent_loop(messages: list):
while True:
# 1. 调用 LLM,传入当前包含历史的完整消息
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
# 2. 将助手的回复(可能是文本或工具调用请求)加入历史
messages.append({"role": "assistant", "content": response.content})
# 3. 【核心判断】如果模型不再要求调用工具,退出循环
if response.stop_reason != "tool_use":
return
# 4. 模型要求调用工具,执行它要求的每一个工具
results = []
for block in response.content:
if block.type == "tool_use":
output = run_bash(block.input["command"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
# 5. 将工具执行结果作为新消息追加,进入下一轮循环
messages.append({"role": "user", "content": results})
关键机制
stop_reason决定去留:值为"tool_use"表示模型还要继续调工具;为"end_turn"/"stop_sequence"等其它值时表示模型认为任务已完成,循环return退出。max_tokens=8000:限制单轮模型输出长度,防止一次生成过长把上下文撑爆;工具调用请求本身也计入这个额度。- 历史持续累积:
messages是不断增长的列表,工具结果以tool_result追加进去,下一轮模型就能"看到"之前的动作和结果。 - 不含业务约束:核心循环本身只是"调模型 → 跑工具 → 回传"的裸骨架,所有安全、权限、规划等约束都在后续模块(s02~s03 及以后)逐步叠加。
本模块知识点
- 最小智能体 = 一个循环:LLM 不断调用工具,直到自行判断任务完成。
- 主循环:
while True→ 调 LLM → 若stop_reason != "tool_use"则退出 → 否则执行工具、结果回传、进入下一轮。 - 工具结果以
tool_result消息追加进messages,历史持续累积。 - 核心循环本身不含任何业务约束,约束在后续模块(s02~s03)逐步叠加。
s02:Tool Expansion(工具扩展)
核心思路
新增了 4 个专用工具:read_file、write_file、edit_file、glob。这为模型提供了结构化的文件操作能力,让模型可以直接表达"读取这个文件"的意图,而无需绕过 shell。工具集从"一个通用接口(bash)“演变为"一套专用 API”。
| 工具 | 作用 | 替代的 shell 写法 |
|---|---|---|
read_file |
读取文件内容 | cat file |
write_file |
写入 / 覆盖文件 | echo ... > file |
edit_file |
精确修改文件片段 | sed / 手动改 |
glob |
按模式查找文件 | find / ls *.py |
工具分发映射:Tool Dispatch Map
遵循开闭原则:对扩展开放,对修改封闭。新增一个工具,只需要三步,核心循环代码无需改动:
- 实现函数:写出工具背后的真实逻辑(如
run_read)。 - 在
TOOLS列表定义:把工具名、描述、输入 schema 注册给模型看。 - 在
TOOL_HANDLERS字典注册:把工具名映射到处理函数。
# 工具扩展
TOOLS = [
{"name": "bash", "description": "Run a shell command.", ...},
{"name": "read_file", "description": "Read file contents.", ...},
...
]
# 字典:工具名称到具体处理函数的映射
TOOL_HANDLERS = {
"bash": run_bash,
"read_file": run_read,
...
}
工作区安全模型:Workspace Safety
safe_path 函数实现了一个沙箱模型:将所有文件操作强制限制在 WORKDIR(当前工作目录)之内。模型传出的任何试图访问工作区外部的路径(如 ../../etc/passwd),都会被 safe_path 的 is_relative_to 检查拦截并抛出异常。这是一种最小权限原则的实践。
# 文件系统的安全守卫
def safe_path(p: str) -> Path:
# 将工作目录与用户路径拼接,形成绝对路径的雏形
# resolve 解析所有 ..、. 符号链接等,得到最终的绝对路径
path = (WORKDIR / p).resolve()
# 解析后的最终路径是否还在工作目录的范围之内
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
TOOLS 与 TOOL_HANDLERS 必须一一对应
TOOLS列表是给 LLM 看的"菜单",定义了工具的接口长什么样(名字、描述、参数)。
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
TOOL_HANDLERS字典是给程序自己看的"后端逻辑",将菜单上的名字映射到真实函数。
# 核心循环中,执行工具变成了通过查表动态分发
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input)
两者必须严格同步:若某工具只在
TOOLS注册却忘了写进TOOL_HANDLERS,模型会去调用它,但handler取到None,落到f"Unknown: {block.name}"分支;若只在TOOL_HANDLERS注册却没进TOOLS,模型根本不知道有这个工具,永远不会调用。
本模块知识点
- 工具集从单一
bash演进为专用 API:read_file/write_file/edit_file/glob。 - 开闭原则:新增工具 = 实现函数 + 在
TOOLS注册 + 在TOOL_HANDLERS字典注册,核心循环不变。 TOOLS(给 LLM 看的菜单)与TOOL_HANDLERS(程序后端逻辑)必须严格一一对应。- 工作区安全:
safe_path用is_relative_to(WORKDIR)实现沙箱,越界路径直接抛异常(最小权限原则)。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 茯茶养生人的博客!









