模型是Agent,代码是harness
Agent = 一个神经网络,学会了感知,推理,行动。(训练好的模型)
核心公式:Agent是模型,代码是Harness
模型(Agent)做决策,做推理,有智能,有目标感。
代码(Harness)提供工具,提供知识,管理上下文,执行决策。
Harness = Tools + knowledge + observation + Action + permissions
类比驾驶者与载具。
模型就是驾驶者,有智能,有判断力,有目标感,决定去哪,怎么去。
而harness就是载具,有方向盘,油门,刹车,导航,传感器,执行驾驶者的决策
你不是在开发agent,你是在
构建Harness
Harness工程师的五项工作
- 1 实现工具:给Agent双手,比如文件读写,Shell,API
- 2 策划知识:给Agent领域专长,按需加载,不前置塞入。
- 3 管理上下文:给Agent干净记忆,隔离,压缩,持久化
- 4 控制权限:给Agent边界,沙箱,审批,信任边界。
- 5 收集数据:每条行动序列,训练下一代模型的原材料。
为什么拆解Claude Code?
因为他是最优雅的Agent Harness,不是因为某个巧妙的技巧,是因为它没做:
- 没有强加 僵化的工作流
- 没有用决策树 替模型判断
- 没有视图成为Agent本身。
他单纯只是:提供工具+知识+上下文+权限,然后让开交给大模型自己来。
Claude Code = Harness机制的组合
Agent = Claude (模型)
Harness = Agent Loop(循环) + Tools(工具) + Skill Loading(按需知识) + Context Compression(上下文压缩) + Subagent(子Agent派生) + Task System(任务图) + Worktree Isolation(隔离执行) + Permission Governance(权限治理) + ...
实现细节
- 1-2 Agent Loop + Tool Use (循环基础:循环+工具派发)
- 3-6 TodoWrite + SubAgent + Skills + compact (规划与知识:规划+隔离+知识+压缩)
- 7-8 Tasks + Backgrorund (持久化:任务+后台)
- 9-12: Trams + protocols + autonomous + worktree (团队协作:邮箱+协议+自治+隔离)
一 Agent Loop
一个循环 +一个工具 = 一个智能体
大模型能推理,但不能触碰真实世界。解决方案:让LLM能自动调用工具并且获取反馈,自动循环,程序驱动,自动闭环。
Agent Loop流程图
User Prompt -> LLM -> Tool -> ToolResult -> LLM-> 可能继续调用工具,可能停止调用,输出。
一个退出条件控制整个流程,如果stop_reason = tool_use,继续循环,执行工具,如果stop_reason = end_turn,结束循环,返回最终文本。
messages[] 累积式消息列表
他就是一个python数组,每轮对话,列表不断增长,这就是Agent的记忆。
代码实现
1 创建tools,这里采用openai风格的tools配置
python
# 1 告诉LLM有一个bash的工具,接收一个command参数
TOOLS = [{
"type": "function",
"function": {
"name": "bash",
"description": "Run a shell command",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string"}
},
"required": ["command"]
}
}
}]
2 工具执行函数
python
# 2 工具执行函数 run_bash()
def run_bash(command: str) -> str:
# 安全过滤:危险命令拦截
dangerous = ["rm -rf /", "sudo", "shutdown", ]
if any(d in command for d in dangerous):
return "Error: Dangerous command blocked"
# 执行命令,开启子进程执行命令
import subprocess
r = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=120)
# 返回stdout + stderr,截断到50kb
return (r.stdout + r.stderr).strip()[: 50000]
主要是创建一个子进程来执行对应的bash命令,并且对危险命令进行拦截,然后对打印信息进行截断,防止挤爆上下文
3 关键的循环
python
# 3 agent loop
def agent_loop(messages: list):
round_num = 0
while True:
round_num += 1
print(f"\n========== 第 {round_num} 轮 ==========")
# 1 调用 LLM
print(f"[1] 调用 LLM,当前 messages 共 {len(messages)} 条")
response = client.chat.completions.create(model="glm-4.5-air", messages=messages, tools=TOOLS)
msg = response.choices[0].message
finish_reason = response.choices[0].finish_reason
print(f"[2] LLM 返回,finish_reason={finish_reason}")
print(f" content: {msg.content}")
if msg.tool_calls:
for tc in msg.tool_calls:
print(f" tool_call: {tc.function.name}({tc.function.arguments})")
# 2 追加助手响应(含 tool_calls 字段)
assistant_msg = {"role": "assistant", "content": msg.content}
if msg.tool_calls:
assistant_msg["tool_calls"] = [
{
"id": tc.id,
"type": "function",
"function": {"name": tc.function.name, "arguments": tc.function.arguments}
} for tc in msg.tool_calls
]
messages.append(assistant_msg)
print(f"[3] 已追加 assistant 消息到 messages")
# 3 不调用工具就结束
if finish_reason != "tool_calls":
print(f"[4] 无工具调用,agent 结束\n")
return
# 4 执行每个工具调用,逐条追加 tool 消息
print(f"[4] 开始执行工具调用,共 {len(msg.tool_calls)} 个")
for tc in msg.tool_calls:
if tc.function.name == "bash":
args = json.loads(tc.function.arguments)
print(f" 执行 bash: {args['command']}")
output = run_bash(args["command"])
print(f" 执行结果: {output[:200]}")
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": output
})
print(f" 已追加 tool 结果到 messages")
# 回到while循环
print(f"[5] 工具执行完毕,进入下一轮")
- 首先我们调用模型,传入我们的问题,模型会返回对应的信息,比如是否需要调用工具等,将返回内容插入messages数组,防止丢失记忆。
- 判断是否调用工具,不调用则表示结束。
- 调用则循环tool_calls,逐一调用工具,如上,使用run_bash执行命令,并且将其返回结果加到messages中,作为tool信息。
- 然后继续循环。
测试:
python
if __name__ == "__main__":
agent_loop(messages=[{
"role": "system",
"content": "你是一个专业的私人助手,在需要的时候调用工具,完成用户的需求"
}, {"role": "user", "content": "帮我创建一个hello.py文件"}])
打印结果:
text
========== 第 1 轮 ==========
[1] 调用 LLM,当前 messages 共 2 条
[2] LLM 返回,finish_reason=tool_calls
content:
我来帮你创建一个hello.py文件,包含一个简单的"Hello, World!"程序:
tool_call: bash({"command":"echo 'print(\"Hello, World!\")' > hello.py"})
[3] 已追加 assistant 消息到 messages
[4] 开始执行工具调用,共 1 个
执行 bash: echo 'print("Hello, World!")' > hello.py
执行结果:
已追加 tool 结果到 messages
[5] 工具执行完毕,进入下一轮
========== 第 2 轮 ==========
[1] 调用 LLM,当前 messages 共 4 条
[2] LLM 返回,finish_reason=tool_calls
content:
tool_call: bash({"command":"cat hello.py"})
[3] 已追加 assistant 消息到 messages
[4] 开始执行工具调用,共 1 个
执行 bash: cat hello.py
执行结果: print("Hello, World!")
已追加 tool 结果到 messages
[5] 工具执行完毕,进入下一轮
========== 第 3 轮 ==========
[1] 调用 LLM,当前 messages 共 6 条
[2] LLM 返回,finish_reason=stop
content:
已经成功为您创建了hello.py文件!文件内容包含了一个简单的Python脚本,会打印"Hello, World!"。
您可以运行这个文件来测试:
python hello.py
或者如果您使用Python 3:
python3 hello.py
[3] 已追加 assistant 消息到 messages
[4] 无工具调用,agent 结束
如上,我们通过一个while循环+工具列表/工具调用+一个finish_reason,则完成了一个最简单的智能体。
二 Tool Use工具使用
上面第一节我们只用了bash,只用bash有什么问题?
- cat读取不可预测,可能撑爆上下文。
- sed编辑文件遇到特殊字符就崩溃
- 安全风险,每次bash调用都是不受约束的安全面。
举个例子,如果只用cat读取,文件如果有10w行,可能会撑爆上下文,如果使用read_file工具我们可以限制limit行数,这就是专用工具的价值。
解决方案:给每个操作定义专用工具
- bash执行命令
- read_file 读取文件
- write_file 写入文件
- edit_file 编辑文件
Dispatch Map-字典分发,使用python字典,替换if else链。
那LLM怎么知道该用哪个工具呢,答案就是每个工具的description。描述写的越清晰,llm就能更近准选中,如果模糊,llm就会执行bash兜底
实现
首先,我们需要创建一个路径访问,而且需要限制他访问的路径
python
# 工作目录沙箱,LLM 只能访问此目录内的文件
WORKDIR = Path(__file__).parent.resolve()
# 4 创建路径沙箱,防止llm读取其他隐私文件
def safe_path(p: str) -> Path:
# 解析绝对路径
path = (WORKDIR / p).resolve()
# is_relative_to检测边界
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
如上,超出当前工作目录的就限制掉。
然后创建我们的工具
python
# 5 定义三个新工具
def run_read(path, limit=None):
text = safe_path(path).read_text()
lines = text.splitlines()
if limit and limit < len(lines):
lines = lines[:limit]
return "\n".join(lines)[:50000]
def run_write(path, content):
fp = safe_path(path)
# parents=True:如果中间层目录不存在也一并创建(类似 mkdir -p)
# exist_ok=True:目录已存在时不报错,静默跳过
fp.parent.mkdir(parents=True, exist_ok=True)
fp.write_text(content)
return f"Wrote {len(content)} bytes to {fp.name}"
def run_edit(path, old_text, new_text):
fp = safe_path(path);
content = fp.read_text();
if old_text not in content:
return "Error: Text not found"
fp.write_text(content.replae(old_text, new_text, 1))
三个工具支持读写改。
接着,我们要定义对应的分发字典,类似于js的对象,策略模式。
python
# 6 定义分发字典
TOOL_HANDLERS = {
"bash": run_bash,
"read_file": run_read,
"write_file": run_write,
"edit_file": run_edit
}
然后给我们的工具Schema增加三种工具
python
TOOLS = [
{
"type": "function",
"function": {
"name": "bash",
"description": "执行一条 shell 命令,返回 stdout + stderr",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "要执行的 shell 命令"}
},
"required": ["command"]
}
}
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取文件内容,可选限制返回行数",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "相对于工作目录的文件路径"},
"limit": {"type": "integer", "description": "最多返回的行数,不传则返回全部"}
},
"required": ["path"]
}
}
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "将内容写入文件,文件不存在则创建,已存在则覆盖",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "相对于工作目录的文件路径"},
"content": {"type": "string", "description": "要写入的文本内容"}
},
"required": ["path", "content"]
}
}
},
{
"type": "function",
"function": {
"name": "edit_file",
"description": "替换文件中第一处匹配的文本片段",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "相对于工作目录的文件路径"},
"old_text": {"type": "string", "description": "要被替换的原始文本"},
"new_text": {"type": "string", "description": "替换后的新文本"}
},
"required": ["path", "old_text", "new_text"]
}
}
}
]
descrption和paremters要写好,描述写得好,llm才更容易懂。
最后替换
python
# 3 agent loop
def agent_loop(messages: list):
round_num = 0
while True:
round_num += 1
print(f"\n========== 第 {round_num} 轮 ==========")
# 调用 LLM
print(f"[1] 调用 LLM,当前 messages 共 {len(messages)} 条")
response = client.chat.completions.create(model="glm-4.5-air", messages=messages, tools=TOOLS)
msg = response.choices[0].message
finish_reason = response.choices[0].finish_reason
print(f"[2] LLM 返回,finish_reason={finish_reason}")
print(f" content: {msg.content}")
print(f" LLM想法: {msg.reasoning_content}")
# 2 追加助手响应(含 tool_calls 字段)
assistant_msg = {"role": "assistant", "content": msg.content}
if msg.tool_calls:
assistant_msg["tool_calls"] = [
{
"id": tc.id,
"type": "function",
"function": {"name": tc.function.name, "arguments": tc.function.arguments}
} for tc in msg.tool_calls
]
messages.append(assistant_msg)
print(f"[3] 已追加 assistant 消息到 messages")
# 3 不调用工具就结束
if finish_reason != "tool_calls":
print(f"[4] 无工具调用,agent 结束 {messages}\n")
return
# 4 执行每个工具调用,逐条追加 tool 消息
print(f"[4] 开始执行工具调用,共 {len(msg.tool_calls)} 个")
for tc in msg.tool_calls:
handler = TOOL_HANDLERS.get(tc.function.name)
# handler = handelr if handelr else TOOL_HANDLERS["bash"]
args = json.loads(tc.function.arguments)
print(f" 执行 {tc.function.name}: {args}")
handler = handler or TOOL_HANDLERS["bash"]
output = handler(**args)
print(f" 执行结果: {output[:200]}")
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": output
})
print(f" 已追加 tool 结果到 messages")
# 回到while循环
print(f"[5] 工具执行完毕,进入下一轮")
主要就是
python
handler = TOOL_HANDLERS.get(tc.function.name)
# handler = handelr if handelr else TOOL_HANDLERS["bash"]
args = json.loads(tc.function.arguments)
print(f" 执行 {tc.function.name}: {args}")
handler = handler or TOOL_HANDLERS["bash"]
output = handler(**args)
handler通过分发字典获取。最后看执行结果
python
if __name__ == "__main__":
agent_loop(messages=[{
"role": "system",
"content": "你是一个专业的私人助手,在需要的时候调用工具,完成用户的需求"
}, {"role": "user", "content": "帮我创建一个greet函数并且测试他"}])
第一轮:
text
========== 第 1 轮 ==========
[0] 当前 messages 共 2 条:
[0] system: 你是一个专业的私人助手,在需要的时候调用工具,完成用户的需求
[1] user: 帮我创建一个greet函数并且测试他
[1] 调用 LLM,当前 messages 共 2 条
[2] LLM 返回,finish_reason=tool_calls
content:
我来帮你创建一个greet函数并测试它。首先创建一个Python文件:
LLM想法:
用户想要我创建一个greet函数并测试它。我需要:
1. 创建一个包含greet函数的文件
2. 写一些测试代码来测试这个函数
我先创建一个Python文件,包含一个简单的greet函数,然后添加一些测试代码。
[3] 已追加 assistant 消息到 messages
[4] 开始执行工具调用,共 1 个
执行 write_file: {'path': 'greet_function.py', 'content': 'def greet(name="World"):\n """\n 一个简单的问候函数\n \n Args:\n name (str): 要问候的名字,默认为"World"\n \n Returns:\n str: 问候语\n """\n return f"Hello, {name}!"\n\n# 测试代码\nif __name__ == "__main__":\n # 测试默认问候\n print("测试默认问候:")\n print(greet()) # 应该输出: Hello, World!\n \n # 测试带参数的问候\n print("\\n测试带参数的问候:")\n print(greet("Alice")) # 应该输出: Hello, Alice!\n print(greet("Bob")) # 应该输出: Hello, Bob!\n \n # 测试不同类型的问候\n print("\\n测试不同类型的问候:")\n print(greet("Python")) # 应该输出: Hello, Python!\n print(greet("World")) # 应该输出: Hello, World!\n \n print("\\n所有测试完成!")'}
执行结果: Wrote 599 bytes to greet_function.py
已追加 tool 结果到 messages
[5] 工具执行完毕,进入下一轮
llm需要调用工具 write_file,参数是path,content
第二轮
python
========== 第 2 轮 ==========
[0] 当前 messages 共 4 条:
[0] system: 你是一个专业的私人助手,在需要的时候调用工具,完成用户的需求
[1] user: 帮我创建一个greet函数并且测试他
[2] assistant → tool_calls: write_file({"path":"greet_function.py","content":"def greet(n)
[3] tool(id=call_-7762415718): Wrote 599 bytes to greet_function.py
[1] 调用 LLM,当前 messages 共 4 条
[2] LLM 返回,finish_reason=tool_calls
content:
LLM想法:
现在我已经创建了一个包含greet函数的Python文件。接下来我需要测试这个函数,通过运行这个Python文件来验证它是否工作正常。
[3] 已追加 assistant 消息到 messages
[4] 开始执行工具调用,共 1 个
执行 bash: {'command': 'python greet_function.py'}
执行结果: /bin/sh: python: command not found
已追加 tool 结果到 messages
[5] 工具执行完毕,进入下一轮
第二轮llm知道已经创建文件后,他需要测试,执行bath,参数是python greet_funciton.py,结果报错了,因为我装的是python3,所以报错信息也追加到了message。
第三轮:
python
========== 第 3 轮 ==========
[0] 当前 messages 共 6 条:
[0] system: 你是一个专业的私人助手,在需要的时候调用工具,完成用户的需求
[1] user: 帮我创建一个greet函数并且测试他
[2] assistant → tool_calls: write_file({"path":"greet_function.py","content":"def greet(n)
[3] tool(id=call_-7762415718): Wrote 599 bytes to greet_function.py
[4] assistant → tool_calls: bash({"command":"python greet_function.py"})
[5] tool(id=call_-7762394587): /bin/sh: python: command not found
[1] 调用 LLM,当前 messages 共 6 条
[2] LLM 返回,finish_reason=tool_calls
content:
LLM想法: None
[3] 已追加 assistant 消息到 messages
[4] 开始执行工具调用,共 1 个
执行 bash: {'command': 'python3 greet_function.py'}
执行结果: 测试默认问候:
Hello, World!
测试带参数的问候:
Hello, Alice!
Hello, Bob!
测试不同类型的问候:
Hello, Python!
Hello, World!
llm知道bash命令报错后,又重新调用bash函数,参数是python3 xx,这会执行可以正常输出了,将输出结果加到messages,
最后一轮
python
[5] 工具执行完毕,进入下一轮
========== 第 4 轮 ==========
[0] 当前 messages 共 8 条:
[0] system: 你是一个专业的私人助手,在需要的时候调用工具,完成用户的需求
[1] user: 帮我创建一个greet函数并且测试他
[2] assistant → tool_calls: write_file({"path":"greet_function.py","content":"def greet(n)
[3] tool(id=call_-7762415718): Wrote 599 bytes to greet_function.py
[4] assistant → tool_calls: bash({"command":"python greet_function.py"})
[5] tool(id=call_-7762394587): /bin/sh: python: command not found
[6] assistant → tool_calls: bash({"command":"python3 greet_function.py"})
[7] tool(id=call_-7762402077): 测试默认问候:\nHello, World!\n\n测试带参数的问候:\nHello, Alice!\nHello, Bob!\n\n测试不同类型的问候:\nHello, Python!\nHello, World!\n
[1] 调用 LLM,当前 messages 共 8 条
[2] LLM 返回,finish_reason=stop
content:
完成!我已经为你创建了一个包含 `greet` 函数的 Python 文件并成功测试了它。
函数已经创建并测试成功,可以正常使用了!</think>
我已经成功创建了一个 `greet` 函数并进行了测试!
你可以使用:
- `greet()` - 默认问候 "Hello, World!"
- `greet("你的名字")` - 问候指定的人,如 `greet("张三")` 会输出 "Hello, 张三!"
LLM想法: None
[3] 已追加 assistant 消息到 messages
[4] 无工具调用,agent 结束
最后一轮当llm知道上一轮的python3 xxx运行成功后,就会直接结束返回结果。
现在我们没动循环,只是单纯加了几个工具,就可以让llm的能力更加强壮。
以后如果需要加一个工具,只需要:
- 1 定义一个工具函数
- 2 分发字典加上该工具
- 3 TOOLS加上该工具,记得descrpiton等描述需要详细让llm知道哦。
三 TodoWrite 计划先行
"没有计划的 agent 走哪算哪" -- 先列步骤再动手, 完成率翻倍。
上述我们给了大模型多个工具,如果我们给他一个复杂的任务。LLM可能会:
- 重复步骤,做完的事情又做一遍,浪费token
- 跳过步骤:当调用工具多了之后,上下文变多的话,大模型会跳过关键步骤,结果不完整。
- 跑偏发散:上下文变多了之后,容易做着做着就去干别的,偏离目标。
具体表现在:如果有10步重构的步骤,前三步可以正常做,到第四步开始就会即兴发挥,因为工具的结果不断填满上下文,系统提示的影响力逐渐被稀释。
解决方案:给Agent一个待办清单工具
没有todo工具的时候
- 接到任务,直接开干
- 做完一步,想起下一步
- 做到中间,忘了还有什么
- 对话变长,完全迷失
有了todo工具后, - llm接到任务,会先列计划,
- 然后逐项完成,标记完成
- 任何时候,都会知道进度
- 有条不絮,不遗漏步骤。
架构
llm循环完全不懂,tool工具增加一个todo工具。
然后新增一个TodoManager todo状态管理,每次agent调用的时候都会更新TodoManager的状态。
此外还有一个Nag Reminder机制,要是连续三轮不更新计划,系统就会强制提示大模型,需要调用todo工具。
每个任务有三个状态
代办: 【】
进行中:【>】
已完成: 【x】
关键约束
同一时间,只能有一个任务在进行中,其他任务不能并行执行,这不是限制,而是保障,多个同时进行的话,agent可能在多个任务之间反复跳转。
代码实现
首先需要创建一个TodoManager类
python
# 定义todo工具
class TodoManager:
items: list
def __init__(self):
self.items = []
# 检验text非空,检验status合法,in_progress <= 1
def update(self, items: list) -> str:
if len(items) > 20:
raise ValueError("Max 20 todos allowed")
validated = []
in_progress_count = 0
for i, item in enumerate(items):
text = str(item.get("text")).strip()
status = str(item.get("status", "pending"))
item_id = str(item.get("id", str(i + 1)))
# 检测text
if not text:
raise ValueError("text required")
# 检测状态
if status not in ("pending", "in_progress", "completed"):
raise ValueError(f"Item {item_id}: invalid status '{status}'")
if status == "in_progress":
in_progress_count += 1
validated.append({"id": item["id"], "text": item["text"], "status": status})
# 只允许有一个在运行
if in_progress_count > 1:
raise ValueError("Only one in_progress")
self.items = validated
return self.render()
# 渲染 todo 列表为文本
def render(self) -> str:
if not self.items:
return "No todos";
lines = []
for item in self.items:
marker = {
"pending": "[ ]",
"in_progress": "[>]",
"completed": "[x]"
}[item["status"]]
lines.append(f"{marker} #{item["id"]} {item["text"]}")
done = sum(1 for t in self.items if t["status"] == "completed")
lines.append(f"({done}/{len(self.items)} completed)")
return "\n".join(lines);
TODO = TodoManager()
主要有两个方法,update就是工具的执行函数,主要是检测状态等是否符合,判断只能有一个任务在执行中,其次render主要是渲染todo列表为文本。
往分发字典增加工具
python
# 6 定义分发字典
TOOL_HANDLERS = {
"bash": run_bash,
"read_file": run_read,
"write_file": run_write,
"edit_file": run_edit,
# 新增分发字典
"todo": TODO.update,
}
接着是往TOOLS增加一个工具描述schema
json
{
"type": "function",
"function": {
"name": "todo",
"description": "更新待办任务列表,每次传入完整的任务列表来替换当前状态;同一时间只能有一个任务处于 in_progress",
"parameters": {
"type": "object",
"properties": {
"items": {
"type": "array",
"description": "完整的任务列表,替换当前所有 todo",
"items": {
"type": "object",
"properties": {
"id": {"type": "string", "description": "任务唯一编号,如 '1'、'2'"},
"text": {"type": "string", "description": "任务内容描述"},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"],
"description": "任务状态:pending=待办,in_progress=进行中,completed=已完成"
}
},
"required": ["id", "text", "status"]
}
}
},
"required": ["items"]
}
}
}
然后在我们的循环加几行代码即可。
python
# 3 agent loop
def agent_loop(messages: list):
round_num = 0
# 检测todo
rounds_since_to = 0
while True:
round_num += 1
print(f"\n========== 第 {round_num} 轮 ==========")
# 调用 LLM
print(f"[1] 调用 LLM,当前 messages 共 {len(messages)} 条")
response = client.chat.completions.create(model="glm-4.5-air", messages=messages, tools=TOOLS)
......
# 2 追加助手响应(含 tool_calls 字段)
# 3 不调用工具就结束
if finish_reason != "tool_calls":
print(f"[4] 无工具调用,agent 结束\n")
return
# 4 执行每个工具调用,逐条追加 tool 消息
used_todo = False # 新增内容!!!!!
print(f"[4] 开始执行工具调用,共 {len(msg.tool_calls)} 个")
for tc in msg.tool_calls:
if tc.function.name == "todo": # 判断是否调用todo工具
used_todo = True # 新增内容!!!!!
......
rounds_since_to = 0 if used_todo else rounds_since_to + 1 # 重置计数器
if rounds_since_to >= 3:# 超过三轮没调用,加入一个messages提醒llm调用todo工具。
messages.append({"role": "user", "content": "<reminder>Update your todos.</reminder>"})
# 回到while循环
print(f"[5] 工具执行完毕,进入下一轮")
判断大模型知否超过三个循环没有调用todo了,是的话强制加入一条messages让其调用todo工具。
测试:
python
if __name__ == "__main__":
agent_loop(messages=[{
"role": "system",
"content": "你是一个专业的私人助手,在需要的时候调用工具,完成用户的需求"
}, {"role": "user", "content": "构建一个爬虫脚本,爬取https://httpbin.org/html"}])
结果:
python
========== 第 1 轮 ==========
[1] 调用 LLM,当前 messages 共 2 条
[2] LLM 返回,finish_reason=tool_calls
content:
我来帮您构建一个爬虫脚本。首先让我规划一下任务步骤:
[3] 已追加 assistant 消息到 messages
[4] 开始执行工具调用,共 1 个
执行 todo: {'items': [{'id': '1', 'text': '分析目标网站结构和需求', 'status': 'pending'}, {'id': '2', 'text': '设计爬虫脚本架构和技术栈', 'status': 'pending'}, {'id': '3', 'text': '创建爬虫脚本主文件', 'status': 'pending'}, {'id': '4', 'text': '实现HTTP请求功能', 'status': 'pending'}, {'id': '5', 'text': '实现HTML解析功能', 'status': 'pending'}, {'id': '6', 'text': '实现数据存储功能', 'status': 'pending'}, {'id': '7', 'text': '添加错误处理和异常处理', 'status': 'pending'}, {'id': '8', 'text': '测试爬虫脚本功能', 'status': 'pending'}]}
执行结果: [ ] #1 分析目标网站结构和需求
[ ] #2 设计爬虫脚本架构和技术栈
[ ] #3 创建爬虫脚本主文件
[ ] #4 实现HTTP请求功能
[ ] #5 实现HTML解析功能
[ ] #6 实现数据存储功能
[ ] #7 添加错误处理和异常处理
[ ] #8 测试爬虫脚本功能
(0/8 completed)
已追加 tool 结果到 messages
[5] 工具执行完毕,进入下一轮
可以看到,已经执行了todo工具,制定了8个待办任务。
python
========== 第 6 轮 ==========
[1] 调用 LLM,当前 messages 共 12 条
[2] LLM 返回,finish_reason=tool_calls
content:
现在创建爬虫脚本主文件:
[3] 已追加 assistant 消息到 messages
[4] 开始执行工具调用,共 1 个
执行 write_file: {'path': 'spider.py', 'content': '......'}
执行结果: Wrote 4535 bytes to spider.py
已追加 tool 结果到 messages
[5] 工具执行完毕,进入下一轮
到第六轮的时候,已经开始创建文件,这时候工具只会调用write_file,下一轮的时候
python
========== 第 7 轮 ==========
[1] 调用 LLM,当前 messages 共 14 条
[2] LLM 返回,finish_reason=tool_calls
content:
[3] 已追加 assistant 消息到 messages
[4] 开始执行工具调用,共 1 个
执行 todo: {'items': [{'id': '1', 'text': '分析目标网站结构和需求', 'status': 'completed'}, {'id': '2', 'text': '设计爬虫脚本架构和技术栈', 'status': 'completed'}, {'id': '3', 'text': '创建爬虫脚本主文件', 'status': 'completed'}, {'id': '4', 'text': '实现HTTP请求功能', 'status': 'completed'}, {'id': '5', 'text': '实现HTML解析功能', 'status': 'completed'}, {'id': '6', 'text': '实现数据存储功能', 'status': 'completed'}, {'id': '7', 'text': '添加错误处理和异常处理', 'status': 'completed'}, {'id': '8', 'text': '测试爬虫脚本功能', 'status': 'in_progress'}]}
执行结果: [x] #1 分析目标网站结构和需求
[x] #2 设计爬虫脚本架构和技术栈
[x] #3 创建爬虫脚本主文件
[x] #4 实现HTTP请求功能
[x] #5 实现HTML解析功能
[x] #6 实现数据存储功能
[x] #7 添加错误处理和异常处理
[>] #8 测试爬虫脚本功能
(7/8 completed)
已追加 tool 结果到 messages
[5] 工具执行完毕,进入下一轮
又会调用todo工具,同步todo列表,如上,已经在执行第八步了
接下来的三轮,llm要测试爬虫脚本 bash工具,然后要执行两轮read_file工具判断生成的内容是否正确,在第10轮的时候,因为已经超过三轮没有调用todo了,我们强制塞入了一个message
python
========== 第 10 轮 ==========
[1] 调用 LLM,当前 messages 共 20 条
[2] LLM 返回,finish_reason=tool_calls
content:
[3] 已追加 assistant 消息到 messages
[4] 开始执行工具调用,共 1 个
执行 read_file: {'path': 'moby_dick_content.txt'}
执行结果: 标题: xxx
来源: xxx
爬取时间: xxx
字数: xx
字符数: xxx
...
已追加 tool 结果到 messages
超过三轮没有todo了,需要提示llm
所以在第11轮的时候
python
========== 第 11 轮 ==========
[1] 调用 LLM,当前 messages 共 23 条
[2] LLM 返回,finish_reason=tool_calls
content:
[3] 已追加 assistant 消息到 messages
[4] 开始执行工具调用,共 1 个
执行 todo: {'items': [{'id': '1', 'text': '分析目标网站结构和需求', 'status': 'completed'}, {'id': '2', 'text': '设计爬虫脚本架构和技术栈', 'status': 'completed'}, {'id': '3', 'text': '创建爬虫脚本主文件', 'status': 'completed'}, {'id': '4', 'text': '实现HTTP请求功能', 'status': 'completed'}, {'id': '5', 'text': '实现HTML解析功能', 'status': 'completed'}, {'id': '6', 'text': '实现数据存储功能', 'status': 'completed'}, {'id': '7', 'text': '添加错误处理和异常处理', 'status': 'completed'}, {'id': '8', 'text': '测试爬虫脚本功能', 'status': 'completed'}]}
执行结果: [x] #1 分析目标网站结构和需求
[x] #2 设计爬虫脚本架构和技术栈
[x] #3 创建爬虫脚本主文件
[x] #4 实现HTTP请求功能
[x] #5 实现HTML解析功能
[x] #6 实现数据存储功能
[x] #7 添加错误处理和异常处理
[x] #8 测试爬虫脚本功能
(8/8 completed)
已追加 tool 结果到 messages
[5] 工具执行完毕,进入下一轮
执行了todo工具,对todo列表进行完善处理。
核心要点
- TodoManager 结构化的进度追踪
- 单in_progress 强制顺序聚焦
- Nag Reminder 问责压力,不更新就催。