从零手写多Agent编排框架:AI Agent Harness 工程化实战指南

在 AI 应用开发进入深水区后,单纯依赖 Dify、Coze 等平台搭建的固定 Workflow 已经难以满足复杂多变的业务需求。正如文档中所言:固定 Workflow 是预先写死的步骤,只能按预设路径执行,无法应对不确定、动态变化的任务。

于是,Agent Harness 工程化 应运而生。它的核心目标是:用工程化手段让 AI Agent 可靠、持续完成任务。

今天,我们将结合一套实战代码,深度拆解现代 AI Agent Harness 体系的核心架构,带你手写一个支持 Sub-Agents(子智能体)的多 Agent 编排框架。


一、 AI Agent Harness 核心架构拆解

一个标准、健壮的 Agent Harness 工程体系,其最小内核包含 6 大基础模块:

  1. Loop 主循环:Agent 的主执行流。核心控制层,负责调用 LLM、分发工具调用、判断任务终止条件。包含 ReAct 机制,让 AI 自主长时间干活。
  2. Tool 工具集:LLM 能使用的工具集(如文件读写、Shell 执行、网络请求等)。
  3. Context 上下文管理器:无状态,负责组装输入给 LLM 的全部内容(系统提示词、历史对话、RAG 检索结果等),并负责上下文压缩与窗口控制。
  4. Environment 沙箱环境:工具运行时的隔离环境,包含文件系统、网络、权限和工作区隔离。
  5. Memory 记忆层:负责跨轮次、跨会话的状态持久化,分为短期记忆和长期记忆。
  6. Observability 可观测性:包含日志分析、Trace、评估指标、调试、回放与评测。

除了这 6 大基础模块,现代 Agent Harness 体系普遍会加入一个高级编排扩展:Sub-Agents(子智能体) ,这也是我们今天实战的重点。

为什么需要 Sub-Agents?

在复杂任务中(如做一份行业技术调研报告),如果全部由主 Agent 完成,会导致主 Agent 上下文极度膨胀、干扰严重。

引入 Sub-Agents 后,架构变成了:

  • 主 Agent:负责任务拆解与整体调度。它的上下文不应收到太多细节的干扰。
  • 子 Agent :负责独立执行子任务(如检索资料、分析数据、生成报告)。它拥有全新的上下文,按需加载工具,绝不干扰主 Agent。

通过这种"子进程"式的隔离,主 Agent 不会因为子 Agent 的运行上下文受拖累,这正是多 Agent 业务隔离的精髓所在。


二、 核心代码实战:构建 Sub-Agent 架构

接下来,我们直接切入代码(基于 OpenAI SDK 与 DeepSeek 模型),看一套完整的 Sub-Agent 系统是如何落地的。

1. 环境初始化与系统提示词定义

首先,初始化客户端并定义工作目录。注意,代码中严格遵守了 Python 的弱类型原则与类型注解:

python

ini 复制代码
import os
import re
import subprocess
from pathlib import Path
import json
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv(override=True)

WORKDIR = Path.cwd()
MODEL = os.getenv("DEEPSEEK_MODEL")

client = OpenAI(
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url=os.getenv("DEEPSEEK_BASE_URL"),
)

# 主Agent与子Agent的系统提示词
SYSTEM = (
    f"You are a coding agent at {WORKDIR}."
    "Use task to delegate focused exploration or a self-contained subtask."
)
SUB_SYSTEM = (
    f"You are a coding agent at {WORKDIR}."
    "Complete the given task, then return a concise final answer."
)

2. 安全沙箱:路径校验与命令拦截

Agent 拥有执行命令和读写文件的能力,因此安全(Policy & Safety) 是必不可少的模块。

路径穿越防护(safe_path) :

防止 LLM 通过 ../../ 等相对路径越权访问非工作目录文件。

python

python 复制代码
def safe_path(p: str) -> Path:
    # python 有个原则:简洁,人生苦短,我用python
    # pathlib.Path 特有的 / 运算符,不是除法,相当于路径拼接
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

命令执行(run_bash) :

在沙箱环境中,我们需要拦截危险命令,防止 AI 误操作导致系统崩溃:

python

python 复制代码
def run_bash(command: str) -> str:
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    
    try:
        r = subprocess.run(
            command, shell=True, cwd=WORKDIR,
            capture_output=True, text=True, errors="replace", timeout=120
        )
        # 三元运算符表达
        out = (r.stdout + r.stderr).strip()
        return out[:50000] if out else "(no output)"
    except subprocess.TimeoutExpired:
        return "Error: Timeout (120s)"
    except (FileNotFoundError, OSError) as e:
        return f"Error: {e}"

3. 基础工具集:文件读写与编辑

Agent 需要与文件系统交互。这里实现了三个核心工具:run_read、run_write、run_edit。

python

python 复制代码
def run_read(path: str, limit: int = None) -> str:
    try:
        lines = safe_path(path).read_text(encoding="utf-8").splitlines()
        if limit and limit < len(lines):
            lines = lines[:limit] + [f"... ({len(lines)-limit} more)"]
        return "\n".join(lines)[:50000]
    except Exception as e:
        return f"Error: {e}"

def run_write(path: str, content: str) -> str:
    try:
        fp = safe_path(path)
        # 获取文件夹,parents=True 自动创建多级父目录
        fp.parent.mkdir(parents=True, exist_ok=True)
        fp.write_text(content, encoding="utf-8")
        return f"Wrote {len(content)} bytes"
    except Exception as e:
        return f"Error: {e}"

def run_edit(path: str, old_text: str, new_text: str) -> str:
    try:
        fp = safe_path(path)
        content = fp.read_text(encoding="utf-8")
        if old_text not in content:
            return f"Error: Text not found in {path}"
        # 替换,只替换第1处匹配的旧文本
        fp.write_text(content.replace(old_text, new_text, 1), encoding="utf-8")
        return f"Edited {path}"
    except Exception as e:
        return f"Error: {e}"

4. 工具 Schema 定义与巧妙映射

LLM 需要根据 JSON Schema 来理解工具。我们将工具划分为 CHILD_TOOLS(子Agent使用的基础工具)和 PARENT_TOOLS(主Agent使用,额外包含 task 任务分配工具)。

python

ini 复制代码
CHILD_TOOLS = [
    # ... 包含 bash, read_file, write_file, edit_file 的 JSON Schema 定义
]

PARENT_TOOLS = CHILD_TOOLS + [
    {
        "type": "function",
        "function": {
            "name": "task",
            # 给子Agent 分配完全独立的上下文
            "description": "Spawn a subagent with fresh context. It shares the filesystem but not conversation history",
            "parameters": {
                "type": "object",
                "properties": {
                    "description": {"type": "string", "description": "Short description of the task"},
                    "prompt": {"type": "string", "description": "Full instructions for the subagent"}
                },
                "required": ["prompt"]
            }
        }
    }
]

工具与执行函数的映射字典 (这里用了非常 Pythonic 的 lambda 表达式):

python

python 复制代码
# 工具名字和对应的执行函数映射字典
# lambda表达式 可以用于简洁的创造匿名函数
# 相当于js里的 () => {}
# **kw 代表 js...kw rest 运算符
TOOL_HANDLERS = {
    "bash": lambda **kw: run_bash(kw["command"]),
    "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
    "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
    "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}

5. 子智能体执行引擎(run_subagent)

这是 Sub-Agent 架构的核心。当主 Agent 调用 task 工具时,会触发 run_subagent。

注意它的上下文是完全独立的(sub_messages 重新开始),且最多尝试 30 次。

python

ini 复制代码
def run_subagent(prompt: str) -> str:
    sub_messages = [{"role": "user", "content": prompt}]
    
    # 独立的 Agentic Loop,最多尝试30次
    for _ in range(30):
        response = client.chat.completions.create(
            model=MODEL,
            messages=[{"role": "system", "content": SUB_SYSTEM}] + sub_messages,
            tools=CHILD_TOOLS,
            max_tokens=8000
        )
        msg = response.choices[0].message
        sub_messages.append(msg.model_dump())  # 消息对象转为 Python 字典,加到对话历史
        
        if response.choices[0].finish_reason != "tool_calls":
            break
            
        results = []
        for tool_call in msg.tool_calls:
            func_name = tool_call.function.name
            # json 格式的工具参数字符串,装成 Python 字典
            args = json.loads(tool_call.function.arguments)
            handler = TOOL_HANDLERS.get(func_name)
            output = handler(**args) if handler else f"Unknown tool: {func_name}"
            results.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(output)
            })
        sub_messages.extend(results)
        
    return msg.content or "(no summary)"

6. 主 Agent 主循环(agent_loop)

最后,回到主 Agent 的循环。主 Agent 拥有 PARENT_TOOLS。当它发现需要执行子任务时,就会调用 task 工具,并将任务转发给子 Agent。

python

ini 复制代码
def agent_loop(messages: list):
    while True:
        response = client.chat.completions.create(
            model=MODEL,
            messages=[{"role": "system", "content": SYSTEM}] + messages,
            tools=PARENT_TOOLS,
            max_tokens=8000
        )
        msg = response.choices[0].message
        messages.append(msg.model_dump())
        
        if response.choices[0].finish_reason != "tool_calls":
            return
            
        results = []
        for tool_call in msg.tool_calls:
            func = tool_call.function
            args = json.loads(func.arguments)
            
            # 主 Agent 分任务
            if func.name == "task":
                desc = args.get("description", "subtask")
                prompt = args.get("prompt", "")
                print(f"> task({desc}): {prompt[:80]}")
                # 启动子 Agent,只关注结果
                output = run_subagent(prompt)
            else:
                # 主 Agent 自己也可以执行任务
                handler = TOOL_HANDLERS.get(func.name)
                output = handler(**args) if handler else f"Unknown tool: {func.name}"
                
            results.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(output)
            })
        messages.extend(results)

7. 交互入口

一个简单的 CLI 循环,接收用户输入并交给主 Agent:

python

python 复制代码
if __name__ == "__main__":
    print("Subagent - fresh messages, final text returns")
    print("Enter a question, press Enter to send. Type q to quit.\n")
    history = []
    while True:
        try:
            query = input(">> ")
        except (EOFError, KeyboardInterrupt):
            break
        if query.strip().lower() in ("q", "exit", ""):
            break
        history.append({"role": "user", "content": query})
        agent_loop(history)

三、 总结与工程化思考

通过上述代码,我们实现了一个极简但五脏俱全的 Harness Agent 体系:

  1. 职责清晰的任务拆分:主 Agent 负责规划,子 Agent 负责执行。
  2. 严格的业务隔离:主 Agent 拥有全局视野但上下文干净;子 Agent 专注具体任务,拥有全新的上下文,不干扰主流程。
  3. 安全可控 :通过 safe_path 和危险命令拦截,保障沙箱环境的安全。
  4. 按需加载 :主 Agent 与子 Agent 分别使用不同的工具集(PARENT_TOOLS vs CHILD_TOOLS),避免一次性加载过多工具导致大模型幻觉。

正如文档最后提到的,不要全信资料,网上有些要分辨的。在更复杂的场景下(如资料检索、校验、生成报告),我们还可以引入更多的 Sub-Agents 进行"反扒"和"资料优劣"的自动分析。

Agent 的工程化之路才刚刚开始。除了 Loop、Tool、Context 等基础模块,未来在生产环境中,我们还需要深入建设 Observability(可观测性) 和 Memory(长期记忆层) ,结合 RAG 和 es 上下文,让 Agent 真正成为可靠的生产力工具。

相关推荐
文谦南宁AI普工1 小时前
从 least privilege 到 least agency:给 Agent 的凭证做一次"不该有权限"的审计
aigc·ai编程
疯狂的魔鬼1 小时前
写给 AI 的约束,为什么写了 40 条也没用
ai编程·vibecoding
用户539418729071 小时前
Cursor 用着用着就抽风?补全失灵、Agent 乱改、一直转圈、额度狂掉,我整理了一张对症排查表
ai编程·cursor
ZzT2 小时前
一周涨 24 万星的 REA,新手和逆向老手为什么评价相反
安全·ai编程
李剑一2 小时前
“79岁陶华碧靠AI一年赚54亿”?别急着转发,先看一瓶辣椒酱的“AI含量”有多高
aigc·ai编程
莪_幻尘2 小时前
全网都在聊的 FDE,到底是啥?一篇拆透
aigc·ai编程
Quz2 小时前
记录第一次使用Pi Agent,以及插件怎么选
agent·ai编程
从小白到软工2 小时前
4-1 指针
c语言·开发语言·数据结构·经验分享·ai编程
挖掘狂人2 小时前
AI 写代码越来越快,我们的代码能力却在悄悄退化
人工智能·程序员·ai编程