在 AI 应用开发进入深水区后,单纯依赖 Dify、Coze 等平台搭建的固定 Workflow 已经难以满足复杂多变的业务需求。正如文档中所言:固定 Workflow 是预先写死的步骤,只能按预设路径执行,无法应对不确定、动态变化的任务。
于是,Agent Harness 工程化 应运而生。它的核心目标是:用工程化手段让 AI Agent 可靠、持续完成任务。
今天,我们将结合一套实战代码,深度拆解现代 AI Agent Harness 体系的核心架构,带你手写一个支持 Sub-Agents(子智能体)的多 Agent 编排框架。
一、 AI Agent Harness 核心架构拆解
一个标准、健壮的 Agent Harness 工程体系,其最小内核包含 6 大基础模块:
- Loop 主循环:Agent 的主执行流。核心控制层,负责调用 LLM、分发工具调用、判断任务终止条件。包含 ReAct 机制,让 AI 自主长时间干活。
- Tool 工具集:LLM 能使用的工具集(如文件读写、Shell 执行、网络请求等)。
- Context 上下文管理器:无状态,负责组装输入给 LLM 的全部内容(系统提示词、历史对话、RAG 检索结果等),并负责上下文压缩与窗口控制。
- Environment 沙箱环境:工具运行时的隔离环境,包含文件系统、网络、权限和工作区隔离。
- Memory 记忆层:负责跨轮次、跨会话的状态持久化,分为短期记忆和长期记忆。
- 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 体系:
- 职责清晰的任务拆分:主 Agent 负责规划,子 Agent 负责执行。
- 严格的业务隔离:主 Agent 拥有全局视野但上下文干净;子 Agent 专注具体任务,拥有全新的上下文,不干扰主流程。
- 安全可控 :通过
safe_path和危险命令拦截,保障沙箱环境的安全。 - 按需加载 :主 Agent 与子 Agent 分别使用不同的工具集(
PARENT_TOOLSvsCHILD_TOOLS),避免一次性加载过多工具导致大模型幻觉。
正如文档最后提到的,不要全信资料,网上有些要分辨的。在更复杂的场景下(如资料检索、校验、生成报告),我们还可以引入更多的 Sub-Agents 进行"反扒"和"资料优劣"的自动分析。
Agent 的工程化之路才刚刚开始。除了 Loop、Tool、Context 等基础模块,未来在生产环境中,我们还需要深入建设 Observability(可观测性) 和 Memory(长期记忆层) ,结合 RAG 和 es 上下文,让 Agent 真正成为可靠的生产力工具。