豆包 Agent Harness 工程师入门 | 第 4 章 子 Agent

3 阅读5分钟

s04 子 Agent

上一章我们使用任务列表的方式来帮助 AI 记忆任务进度,这样就可以让 AI 完成更复杂的任务。

image.png

但是如果任务再复杂一点呢,如果完成任务所需的文本量超过了 AI 的上下文了咋办,比如上述的模型,大多数都是 256k 的上下文,如果超过了这个上下文,那么就算用任务列表提醒 AI ,AI 也是会忘记前面内容的细节、过程等的内容。

为了解决这个问题,我们可以引入一个新的子 Agent ,让它来分担一部分内容。

我们可以让主 Agent 来分解任务、追踪任务。让子 Agent 去完成具体的每一项任务。然后把任务完成情况的结果再返回给主 Agent,这样就可以在主 Agent 256k 的上下文中完成更复杂的任务了。

首先我们先来看看整个程序的流程图:

image.png

然后是完整代码,请移步 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 直接自己就给执行完了,哈哈哈哈,或许是中文提示词和英文工具名导致的吧,总体来说中文提示词还是不咋好用的。 image.png

然后是 result.json 和 subagent_result.josn 分别放了 AI 和子 Agent 的历史命令消息列表,可以去 gitee 仓库查看。 image.png

image.png