s04 子 Agent
上一章我们使用任务列表的方式来帮助 AI 记忆任务进度,这样就可以让 AI 完成更复杂的任务。
但是如果任务再复杂一点呢,如果完成任务所需的文本量超过了 AI 的上下文了咋办,比如上述的模型,大多数都是 256k 的上下文,如果超过了这个上下文,那么就算用任务列表提醒 AI ,AI 也是会忘记前面内容的细节、过程等的内容。
为了解决这个问题,我们可以引入一个新的子 Agent ,让它来分担一部分内容。
我们可以让主 Agent 来分解任务、追踪任务。让子 Agent 去完成具体的每一项任务。然后把任务完成情况的结果再返回给主 Agent,这样就可以在主 Agent 256k 的上下文中完成更复杂的任务了。
首先我们先来看看整个程序的流程图:
然后是完整代码,请移步 gitee 查看: gitee.com/sanqiushu/D…
代码变化
SYSTEM = f"""你是一个位于 {WORKDIR} 的编程智能体。请使用 task 工具来委派探索或进行子任务"""
SUBAGENT_SYSTEM = f"""你是一个位于 {WORKDIR} 的编程子代理。请完成指定任务,然后总结你的发现。"""
首先是提示词,现在区分为了系统提示词和子 Agent 提示词,这样主 Agent 就可以让 子 Agent 来帮忙干活了。
CHILD_TOOLS = [ run_bash 、 read_file、 write_file、 edit_file ] # 还是以前的四个工具
PARENT_TOOLS = CHILD_TOOLS + [
{
"type": "function",
"function": {
"name": "task",
"description": "创建一个具有全新上下文的子代理。它共享文件系统,但不共享对话历史记录。",
"parameters": {
"type": "object",
"properties": {
"prompt": {
"type": "string"
},
"description": {
"type": "string",
"description": "对任务的简短描述"
}
},
"required": ["prompt"]
}
}
}
]
task 工具就是主 Agent 来调用创建子 Agent 的工具了。主 Agent 创建 子 Agent 的时候只需要传递给子 Agent prompt 提示词即可。
# Agent 循环的核心方法
def agent_loop(messages: list):
while True:
response = client.chat.completions.create(model=MODEL, messages=messages, tools=PARENT_TOOLS, max_tokens=8000)
# 将大模型返回的消息添加到消息列表中
message = response.choices[0].message
messages.append(message.model_dump())
# 如果大模型没有调用工具,那么结束执行
if response.choices[0].finish_reason != "tool_calls":
# 打印 AI 的响应
print(f"\n\033[36mAI: {message.content}\033[0m\n")
return
# 打印 AI 的响应
print(f"\n\033[36mAI: \033[0m\033[36;9m思考: {message.content}\033[0m\n\033[36m回答:{message.reasoning_content} \033[0m\n")
# if message.tool_calls:
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
call_id = tool_call.id
# 3. 路由并执行本地函数
if func_name == "task":
desc = func_args.get("description", "subtask")
prompt = func_args.get("prompt")
print(f"\n\033[33m子任务: ({desc}): {prompt[:80]}\033[0m\n")
output = run_subagent(prompt)
else:
handler = TOOL_HANDLERS.get(func_name)
output = handler(**func_args) if handler else f"不存在 {func_name} 工具"
print(f"\n\033[33mtools : \n{output}\033[0m\n") # 打印工具的输出
messages.append({"role": "tool", "content": output, "tool_call_id": call_id})
然后是主 Agent 循环,其实变化不大,就是加了下面这段代码:
if func_name == "task":
desc = func_args.get("description", "subtask")
prompt = func_args.get("prompt")
print(f"\n\033[33m子任务: ({desc}): {prompt[:80]}\033[0m\n")
output = run_subagent(prompt)
当主 Agent 调用子 Agent 的时候,我们调用 run_subagent() 函数即可。
def run_subagent(prompt: str) -> str:
sub_messages = [
{ "role": "system", "content": SUBAGENT_SYSTEM },
{ "role": "user", "content": prompt }
]
print("子 Agent 正在运行中....")
for _ in range(30): # 子 Agent 不能跑太久,只允许跑 30 次循环
response = client.chat.completions.create(model=MODEL, messages=sub_messages, tools=CHILD_TOOLS, max_tokens=8000)
message = response.choices[0].message
sub_messages.append(message.model_dump())
if response.choices[0].finish_reason != "tool_calls":
print(f"\n\033[36m子 Agent: {message.content}\033[0m\n")
with open("subagent_result.json", "a") as f:
f.write(json.dumps(sub_messages, indent=4, ensure_ascii=False) + "\n")
break
print(f"\n\033[36m子 Agent: \033[0m\033[36;9m思考: {message.content}\033[0m\n\033[36m回答:{message.reasoning_content} \033[0m\n")
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
call_id = tool_call.id
handler = TOOL_HANDLERS.get(func_name)
output = handler(**func_args) if handler else f"不存在 {func_name} 工具"
print(f"\n\033[33mtools : \n{output}\033[0m\n") # 打印工具的输出
sub_messages.append({"role": "tool", "content": output, "tool_call_id": call_id})
return message.content if message.content else "子代理:没有总结"
这段 run_subagent() 代码就是这一章主要新增的代码了。当新建一个子 Agent 的时候,我们新建一个子历史消息列表,然后将子 Agent 的系统级提示注入进去,然后将主 Agent 传递给子 Agent 的 prompt 提示词也给注入进去即可。每一个子 Agent 都是空白的上下文,这样就可以避免主 Agent 的上下文污染子 Agent。
然后设置了子 Agent 只能独立运行 30 次循环,这里没有处理超过 30 次的情况,后面可以优化一下。
然后子 Agent 循环除了不能调用 task 工具之外,和主 Agent 循环也是一样的。Task 工具不包含在子代理的工具集中。子代理必须直接完成工作,不能继续委派。这防止了无限委派循环:没有这个约束,一个代理可能创建子代理,子代理又创建子代理,每一层都用略微不同的措辞重新委派同一任务,消耗 token 却毫无进展。一层委派足以处理绝大多数场景。如果任务对单个子代理来说太复杂,应该由父代理重新分解。
然后子 Agent 完成任务后,我们只将子 Agent 总结的内容返回给主 Agent 即可,避免读取的文件这些东西污染主 Agent 的上下文。总体来说,子 Agent 完成任务后,主 Agent 的上下文只会增加一个很短的摘要总结,避免了上下文快速膨胀,占满 256k 的上下文空间。
然后下面是程序运行的截图,我这个要求也不复杂,AI 也可以完成,但是如果我不提一下让他用 task 工具来执行,这个 AI 直接自己就给执行完了,哈哈哈哈,或许是中文提示词和英文工具名导致的吧,总体来说中文提示词还是不咋好用的。
然后是 result.json 和 subagent_result.josn 分别放了 AI 和子 Agent 的历史命令消息列表,可以去 gitee 仓库查看。