Agent Harness:从裸调 LLM 到生产级 Agent 的工程实践

大家好,我是程序员小策。

你正在做一个 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 最核心的五个能力:

  1. send_message点对点通信------A 发给 B,等 B 回复。Runtime 内部做了序列化/反序列化/重试/超时,调用方不需要关心。
  2. publish_message广播通信------一条消息多个 Agent 关心。Agent 不需要知道"谁会收到",只需要"往某个 Topic 发"。
  3. register_factory延迟创建------Agent 不预创建,在有消息到达时才创建。生产环境中上百个 Agent,全预创建内存直接炸。
  4. save_state/load_state故障恢复------Agent 跑了一半挂了,重启后恢复到中断前的状态继续跑。
  5. 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(接口),SingleThreadedAgentRuntimeGrpcWorkerAgentRuntime 是两种实现。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 队列传消息,消息格式不统一、没有统一的重试和超时机制。

重构方案

  1. 用 AutoGen 的 AgentRuntime 替代 Redis 队列作为消息总线
  2. 为每个 Agent 绑定专属 Topic:intent_topicorder_topicrefund_topicescalation_topic
  3. 统一在 Harness 层注入 CancellationToken,所有 LLM 调用可取消
  4. 在 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 需要框架"。

相关推荐
wp123_11 小时前
从Coilcraft SER2915L-472KL看国产扁线电感在AI算力等领域的机遇
人工智能
Database_Cool_1 小时前
AI 时代的数据仓库:阿里云 AnalyticDB MySQL 向量检索 + SQL 分析一体化实战
数据仓库·人工智能·mysql·阿里云
羊羊小栈1 小时前
停车场管理系统(基于前后端Web开发)
前端·人工智能·毕业设计·大作业
CodePlayer竟然被占用了1 小时前
开发者正在抛弃 Copilot,转向 AI Loop
人工智能
大模型最新论文速读1 小时前
06-08 · LLM 最新论文速览
论文阅读·人工智能·深度学习·机器学习·自然语言处理
武汉知识图谱科技1 小时前
华为克拉玛依城市超级智能体落地:智慧政务从“上云”到“全域智能”的跃迁路径
人工智能·政务
张彦峰ZYF1 小时前
LangGraph Tool Calling 入门:从 @tool 到完整调用链
人工智能·大模型·agent·langgraph·tool calling
半亩码田1 小时前
06.01-06.07 AI大事件速览 | 扣子3.0、Hinton警告AI有意识、千问3.7-Plus
人工智能