大家好,我是程序员小策。
你正在做一个 AI Agent 项目。
LLM 调用写好了,Prompt 也调了几十版了------角色设定、输出格式、Few-shot 示例,每条 Prompt 都打磨得跟艺术品似的。Context 工程也做了------RAG 检索、对话记忆、工具调用结果拼接,上下文窗口被塞得满满当当。
但一跑起来,还是各种问题:
- LLM 偶尔返回格式错误,你的代码直接崩了,没有重试。
- 工具调用超时 10 秒,Agent 卡在那里不动,用户等了 30 秒没反应就关了页面。
- 两个 Agent 之间传消息,A 发出去 B 没收到,日志里连条错误都没有。
- 想加个"用户点了取消按钮就中止执行",发现每条 LLM 调用都是同步阻塞的,根本取消不了。
问题出在哪?
Prompt 没问题。Context 也没问题。问题是你缺了一层------Agent Harness。
Prompt 是方向盘,决定 Agent 往哪走。Context 是燃料,决定 Agent 能走多远。但如果没有底盘------没有悬挂系统(错误处理)、没有刹车(取消机制)、没有差速器(并发调度)、没有仪表盘(日志观测)------这辆车根本上不了路。
问题定义:裸调 LLM 和 Agent 之间差了什么?
用伪代码对比一下:
裸调 LLM(Demo 阶段):
python
response = llm.chat(prompt)
print(response)
加了 Agent Harness(生产阶段):
python
runtime.send_message(message, recipient=agent_id,
cancellation_token=timeout_token)
# Harness 自动处理:序列化、路由、重试、取消、日志、状态持久化
区别在哪里?三行代码和一行代码的区别不是字数,而是那一行代码背后被自动处理掉的所有事情。
Agent Harness(执行框架):围绕 LLM 构建的一套运行时基础设施,负责消息路由、Agent 生命周期管理、工具调用编排、错误重试、取消机制、状态持久化、日志观测------让 Agent 从"能跑"变成"可靠地跑"。
核心概念:用"造车"理解 Agent Harness
你买了一台 V12 发动机------动力充沛,声音澎湃。但光有发动机你上不了路。
你还需要:
- 底盘 + 悬挂 :承载发动机重量,吸收路面颠簸 === Harness 的错误处理和重试机制
- 方向盘 + 转向系统 :控制方向 === Prompt 工程(决定 Agent 的行为方向)
- 油箱 + 供油系统 :提供燃料 === Context 工程(RAG、记忆、工具结果)
- 刹车系统 :跑太快了能停下 === Harness 的取消和超时机制
- 差速器 + 变速箱 :动力分配到不同轮子 === Harness 的消息路由和 Agent 调度
- 仪表盘 :速度、油量、故障灯 === Harness 的日志、Metrics、Tracing
三者的协作关系:
Prompt 工程(方向盘)
↓ 控制行为
LLM(发动机)← Context 工程(燃料)
↓ 驱动
Agent Harness(底盘+刹车+仪表盘)
↓ 承载
生产级 Agent(能上路的车)
Prompt Engineering 回答"做什么",Context Engineering 回答"用什么做",Agent Harness 回答"怎么做、做错了怎么办、怎么观测做得怎么样"。
实现:以 Microsoft AutoGen 为例拆解 Agent Harness
下面这段代码来自微软开源的多 Agent 框架 microsoft/autogen,我提取了其核心 Runtime 和 BaseAgent 的设计,加详细注释逐层拆解。
4.1 AgentRuntime:Harness 的核心协议
python
from typing import Any, Callable, Mapping, Protocol
from collections.abc import Sequence
class AgentRuntime(Protocol):
"""Agent 执行框架的核心接口------所有 Agent 都运行在 Runtime 之上"""
async def send_message(
self, message: Any, recipient: AgentId,
*, sender: AgentId | None = None,
cancellation_token: CancellationToken | None = None,
message_id: str | None = None,
) -> Any:
"""向指定 Agent 发送消息并等待响应
Harness 自动处理:序列化 → 路由 → 反序列化 → 重试 → 返回"""
...
async def publish_message(
self, message: Any, topic_id: TopicId,
*, sender: AgentId | None = None,
cancellation_token: CancellationToken | None = None,
) -> None:
"""向订阅了某个 Topic 的所有 Agent 广播消息
发布-订阅模式,无返回值,适合"通知所有人"的场景"""
...
async def register_factory(
self, type: str,
agent_factory: Callable[[], Agent],
) -> AgentType:
"""注册 Agent 工厂------Harness 按需创建 Agent 实例
不是预创建所有 Agent,而是"延迟创建"(lazy instantiation)"""
...
async def save_state(self) -> Mapping[str, Any]:
"""保存整个 Runtime 的状态------包括所有 Agent 的内部状态
用于故障恢复:重启后 load_state() 恢复到中断前的状态"""
...
async def load_state(self, state: Mapping[str, Any]) -> None:
"""从保存的状态中恢复 Runtime"""
...
async def add_subscription(self, subscription: Subscription) -> None:
"""添加消息订阅------告诉 Runtime '这个 Agent 对这类消息感兴趣'"""
...
为什么这样设计?
这是 Agent Harness 最核心的五个能力:
send_message是点对点通信------A 发给 B,等 B 回复。Runtime 内部做了序列化/反序列化/重试/超时,调用方不需要关心。publish_message是广播通信------一条消息多个 Agent 关心。Agent 不需要知道"谁会收到",只需要"往某个 Topic 发"。register_factory是延迟创建------Agent 不预创建,在有消息到达时才创建。生产环境中上百个 Agent,全预创建内存直接炸。save_state/load_state是故障恢复------Agent 跑了一半挂了,重启后恢复到中断前的状态继续跑。add_subscription是消息路由规则------Runtime 的"路由器",决定消息该发给谁。
注意这五个能力里,没有一个是 LLM 调用本身。Harness 的价值不在于"调用 LLM",而在于"让 LLM 调用变得可靠"。
4.2 BaseAgent:Agent 如何在 Harness 中运行
python
from abc import ABC, abstractmethod
from typing import Any, ClassVar, List
class BaseAgent(ABC):
"""所有 Agent 的基类------定义了 Agent 如何与 Runtime 交互"""
# 类级别:这个 Agent 类关心哪些类型的消息
_unbound_subscriptions: ClassVar[List[Subscription]] = []
def __init__(self, description: str):
self._description = description
# 在工厂调用上下文中,自动从 Runtime 获取 ID 和 Runtime 引用
# 这就是"依赖注入"------Agent 不自己创建 Runtime,而是 Runtime 注入进来
if AgentInstantiationContext.is_in_factory_call():
self._runtime = AgentInstantiationContext.current_runtime()
self._id = AgentInstantiationContext.current_agent_id()
@property
def id(self) -> AgentId:
return self._id
@property
def runtime(self) -> AgentRuntime:
return self._runtime
@abstractmethod
async def on_message_impl(self, message: Any, ctx: MessageContext) -> Any:
"""子类必须实现:收到消息时做什么"""
...
async def send_message(
self, message: Any, recipient: AgentId,
*, cancellation_token: CancellationToken | None = None,
) -> Any:
"""Agent 之间发消息------委托给 Runtime 处理"""
return await self._runtime.send_message(
message, sender=self.id, recipient=recipient,
cancellation_token=cancellation_token
)
async def save_state(self) -> Mapping[str, Any]:
"""保存 Agent 自身状态(子类可重写)"""
return {}
async def load_state(self, state: Mapping[str, Any]) -> None:
"""恢复 Agent 自身状态(子类可重写)"""
pass
async def close(self) -> None:
"""Agent 关闭时的清理逻辑"""
pass
为什么这样设计?
关键设计决策有三个:
- 依赖注入而非主动创建 :
self._runtime不是Agent.__init__里new出来的,而是通过AgentInstantiationContext从工厂上下文注入的。这样 Agent 和 Runtime 是解耦的------同一个 Agent 类可以跑在单机 Runtime,也可以跑在分布式 Runtime,代码不用改。 on_message_impl是唯一的抽象方法:所有 Agent 只需要实现"收到消息后做什么"。怎么收消息、怎么路由、怎么序列化------全是 Harness 的事。Agent 只需关心业务逻辑。save_state/load_state默认空实现:不是所有 Agent 都需要状态持久化。对话 Agent 需要保存对话历史,但简单的路由 Agent 不需要。设计上给了灵活性。
4.3 组装:Prompt + Context + Harness 三合一
python
from autogen_core import (
AgentRuntime, SingleThreadedAgentRuntime,
TypeSubscription, TopicId
)
from autogen_ext.models.openai import OpenAIChatCompletionClient
class AnalysisAgent(BaseAgent):
"""一个结合了 Prompt 工程和 Context 工程的 Agent"""
def __init__(self):
super().__init__("数据分析 Agent")
# Context 工程:模型客户端 + 工具注册
self._model_client = OpenAIChatCompletionClient(model="gpt-4")
# Prompt 工程:系统提示词定义了 Agent 的行为边界
self._system_prompt = """你是一个数据分析专家。
分析用户提供的数据,按以下格式输出:
1. 关键发现(不超过3条)
2. 数据异常点
3. 建议的下一步行动"""
async def on_message_impl(self, message, ctx):
# Context 工程:拼接上下文
full_context = [
SystemMessage(self._system_prompt), # ← Prompt 工程
UserMessage(message.content), # ← 用户输入
]
result = await self._model_client.create(
messages=full_context,
cancellation_token=ctx.cancellation_token # ← Harness 取消机制
)
return result.content
# Harness 组装:注册 Agent → 绑定路由 → 启动
async def main():
# 创建 Runtime(Harness)
runtime = SingleThreadedAgentRuntime()
# 注册 Agent 工厂------告诉 Harness "这个类型对应这个类"
await AnalysisAgent.register(runtime, "analyst",
lambda: AnalysisAgent())
# 添加消息路由------告诉 Harness "analysis_topic 的消息发给 analyst"
await runtime.add_subscription(
TypeSubscription(topic_type="analysis_topic",
agent_type="analyst")
)
# 启动 Runtime------Harness 开始处理消息
runtime.start()
为什么这样组织?
注意 Prompt、Context、Harness 在这里的职责边界非常清晰:
self._system_prompt是 Prompt 工程------定义了 Agent "做什么、怎么输出"。full_context的拼接是 Context 工程------定义了 Agent "用什么信息来推理"。SingleThreadedAgentRuntime、订阅、取消令牌------这些都是 Harness 的范畴,定义了 Agent "怎么跑"。
三者不是竞争关系,而是分层协作。把 Prompt 当成 Harness(在 Prompt 里写"如果失败请重试"),或者把 Context 当成 Prompt(把所有信息都塞进系统提示词),都是常见的反模式。
边界与陷阱:Agent Harness 实现中五个容易踩的坑
陷阱一:Harness 是异步的,但你的 LLM 调用可能不是。 AutoGen 的 Runtime 全面采用 async/await。如果你在 Agent 里写了同步的 requests.post() 去调 LLM API,会把整个 Runtime 的事件循环堵死。解法:所有 IO 操作都用异步版本。
陷阱二:状态持久化不等于"全量保存"。 save_state() 帮你把 Agent 状态存下来,但如果你在每个消息处理完后都调一次,性能直接崩。解法:只在关键节点(任务完成、用户确认)做持久化,日常用内存缓存。
陷阱三:消息序列化处处是坑。 Harness 在 Agent 间传消息时需要序列化。如果你传了一个自定义的 Pydantic 对象,但没注册序列化器,Runtime 直接报 ValueError。解法:所有跨 Agent 的消息类型都必须注册对应的 MessageSerializer。
陷阱四:取消令牌不是银弹。 CancellationToken 可以取消一个正在执行的 Agent 任务------但前提是 Agent 内部确实在检查令牌状态。如果 Agent 在一个不带超时的 HTTP 请求里卡住了,取消令牌也救不了你。解法:所有外部调用都带上超时参数。
陷阱五:单线程 Runtime 适合开发,不适合生产。 SingleThreadedAgentRuntime 是单线程的------所有 Agent 顺序执行。Demo 够了,生产环境必须切到支持并发的 Runtime 实现。
高级考量:从单机到分布式的 Harness 演进
当你的 Agent 系统从"一台机器跑 10 个 Agent"变成"10 台机器跑 100 个 Agent"时,Harness 本身需要升级:
python
# 开发阶段:单机单线程
runtime = SingleThreadedAgentRuntime()
# 生产阶段:分布式 Runtime(基于 gRPC)
# 不同的 Agent 可以分布在不同的节点上运行
runtime = GrpcWorkerAgentRuntime(host_address="node-1:50051")
await runtime.start()
# 通过服务发现,Agent A 在 node-1 上,Agent B 在 node-2 上
# Runtime 自动处理跨节点的消息路由
AutoGen 的架构就是按这个思路设计的:AgentRuntime 是一个 Protocol(接口),SingleThreadedAgentRuntime 和 GrpcWorkerAgentRuntime 是两种实现。Agent 代码不需要修改,只换 Runtime 实现就能从单机扩展到分布式。
另一个生产级考量是观测性 。Harness 是所有消息的必经之路,所以是插 Metrics 和 Tracing 的最佳位置。每个 send_message 调用都可以记录耗时、成功率、错误类型。这些数据反馈给 Prompt 工程和 Context 工程,形成闭环优化。
项目实战:在智能客服 Multi-Agent 系统中落地 Agent Harness
去年我参与了一个电商智能客服 Multi-Agent 系统的重构------从"裸调 LLM"迁移到带 Harness 的 Agent 架构。
场景:客服系统有 4 个 Agent:意图识别 Agent、订单查询 Agent、退款处理 Agent、人工转接 Agent。原来每个 Agent 都是独立脚本,通过 Redis 队列传消息,消息格式不统一、没有统一的重试和超时机制。
重构方案:
- 用 AutoGen 的
AgentRuntime替代 Redis 队列作为消息总线 - 为每个 Agent 绑定专属 Topic:
intent_topic、order_topic、refund_topic、escalation_topic - 统一在 Harness 层注入
CancellationToken,所有 LLM 调用可取消 - 在 Runtime 层插 Metrics:每个
send_message的耗时和成功率
简化后的核心代码:
python
# 重构前:裸调 LLM,没有统一的消息机制
async def handle_intent(user_message: str) -> str:
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "system", "content": INTENT_PROMPT},
{"role": "user", "content": user_message}]
)
return response.choices[0].message.content
# 重构后:Agent 运行在 Harness 之上
class IntentAgent(BaseAgent):
def __init__(self):
super().__init__("意图识别 Agent")
self._model_client = OpenAIChatCompletionClient(model="gpt-4")
async def on_message_impl(self, message, ctx):
# Prompt 工程:系统提示词
# Context 工程:拼接上下文
# Harness:ctx.cancellation_token 保证可取消
result = await self._model_client.create(
messages=[
SystemMessage(INTENT_PROMPT),
UserMessage(message.content)
],
cancellation_token=ctx.cancellation_token
)
return result.content
实际效果:
- Agent 间消息传递从"Redis 队列 + 手动序列化"变成一行
await runtime.send_message() - 用户取消操作的响应时间从 30 秒降到 1 秒以内(通过 CancellationToken)
- 因为统一了 Harness 层的日志,定位一个问题从"翻 4 个 Agent 各自日志"变成"看 Runtime 的统一 Trace"
- 故障恢复:Agent 崩溃后 Runtime 自动重新创建实例,对话上下文通过
save_state/load_state恢复
踩坑记录:
- 消息类型爆炸。4 个 Agent 定义了 12 种消息类型,每种都要注册序列化器。后来收敛到 4 种核心消息类型 + 1 种通用 fallback 类型。
- LLM 的
create()方法在 AutoGen 里默认有重试机制,但重试间隔是指数退避------第一次重试等 1 秒,第二次等 2 秒,第三次等 4 秒。对于在线客服场景太慢,改成了固定 500ms 重试间隔 + 最多 2 次。 - Runtime 的 add_subscription 是幂等的但
TypePrefixSubscription有坑------如果你忘记在 topic_type_prefix 末尾加:,可能会匹配到不应匹配的 Agent 类型。
对比表格:Agent 架构三支柱
| 支柱 | 核心问题 | 典型技术 | 如果缺失会怎样 | 三者协作方式 |
|---|---|---|---|---|
| Prompt 工程 | Agent 应该做什么?如何思考? | System Prompt、Few-shot、Chain-of-Thought、输出格式约束 | Agent 行为不可控,输出格式混乱 | 定义 Agent 的"任务说明书",Harness 确保这份说明书被正确执行 |
| Context 工程 | Agent 用什么信息来推理? | RAG 检索、对话记忆、工具调用结果拼接、上下文窗口管理 | Agent 信息不足或信息过载,推理质量差 | 提供 Agent 的"燃料",Harness 负责把燃料按时按量输送给 LLM |
| Agent Harness | Agent 怎么稳定可靠地运行? | Runtime、消息路由、重试机制、取消令牌、状态持久化、日志观测 | Agent 能跑但不稳定,出了问题无法定位,无法恢复 | 承载 Prompt 和 Context 的执行,提供底盘的稳定性保障 |
一句话总结:Prompt 决定了 Agent 的上限(能做多聪明的事),Context 决定了 Agent 的宽度(能处理多少信息),Harness 决定了 Agent 的下限(能有多稳定)。
面试追问
追问 1:Agent Harness 和 LangChain 的 Chain/AgentExecutor 是什么关系?
回答方向:Chain/AgentExecutor 是 Harness 的一种具体实现,面向"单 Agent + 工具调用"场景。AutoGen 的 AgentRuntime 面向"Multi-Agent + 事件驱动"场景。前者是顺序执行(A 做完 B 做),后者是消息驱动(谁收到消息谁做)。选择取决于你的 Agent 拓扑结构------线性流水线用 Chain,多 Agent 网状协作用 AgentRuntime。
追问 2:Prompt 工程和 Harness 的边界在哪里?为什么不应该在 Prompt 里写重试逻辑?
回答方向:Prompt 是给 LLM 的指令,Harness 是给代码的指令。在 Prompt 里写"如果输出格式不对,请重新生成"有三个问题:① 不可靠------LLM 可能忽略 ② 不可控------你不知道重试了几次 ③ 不可观测------没有日志记录重试次数。正确做法:Prompt 只管内容,Harness 通过代码做格式校验 + 自动重试------精确、可控、可观测。
追问 3:什么时候应该自己写 Harness,什么时候用现成框架?
回答方向:如果只有一个 Agent、一条 LLM 调用链、不需要 Agent 间通信------自己写简单的重试+日志包装就够了,不需要引入框架。如果你有 3 个以上的 Agent 需要互相发消息、需要状态持久化、需要跨机器部署------一定用现成的 Harness 框架。框架的"重"是有道理的------消息序列化、分布式路由、状态恢复这些不是几行代码能搞定的。
Agent Harness 不是让 LLM 更聪明,而是让 LLM 更可靠。
读完这篇你应该能:说清楚 Prompt 工程、Context 工程、Agent Harness 三者的职责边界、理解 AgentRuntime 的五大核心能力(消息传递、生命周期、状态持久化、路由订阅、取消机制)、用 AutoGen 跑一个带 Harness 的 Multi-Agent Demo、在面试时说出"Harness 是 Agent 的底盘,不是发动机"而不只是"我知道 Agent 需要框架"。