干Agent开发这两年,我最常被问到的问题不是“LangGraph怎么用”,而是“Agent Harness 和 Agent Runtime到底有什么区别”。不光刚入门的人懵,很多已经上线过Agent项目的团队,嘴上说着“运行时”“执行框架”,实际排查问题时还是会把两者混在一起。这篇文章我就把这两个概念彻底拆开讲清楚:它们分别负责什么、边界在哪里、选型怎么考虑,以及在真实项目里到底会遇到哪些坑。
先说结论:Agent Harness管“Agent怎么被组装出来”,Agent Runtime管“组装好的Agent怎么稳定跑起来”。一个是开发态给你搭结构用的框架层,一个是运行态负责调度、状态、恢复和观测的设施层。把这层窗户纸捅破,后面看很多Agent框架的文档都会顺畅不少。
1. 先对齐认知:Agent、Agent Harness、Agent Runtime到底指什么
1.1 Agent其实是“要交付的东西”,不是一层可运行软件
很多人一提到Agent默认就是能对话的机器人,这没问题。但做工程的人必须清楚,Agent本质上是“一个具备感知、决策、行动循环的程序”。它要接收外部输入(用户问题、环境变化、事件触发),要调用大模型来做推理决策,还要执行工具去改变现实或获取信息,最后把结果返回。
在这个定义下,Agent本身是一个“逻辑产物”。我们可以用任何语言把它写出来,也可以只写一个脚本跑完就退出。它是可以被交付的东西,却不是天然具备“长期运行”“故障恢复”“多人并发访问”能力的服务。也就是说,Agent本身不等于一个系统,它更像一个“业务逻辑包”。真正让它在生产环境里活下来的,是外面的两层:一层负责把逻辑拼装得清晰可控,另一层负责给拼装好的东西提供运行环境。
这就自然引出了Agent Harness 与 Agent Runtime的分工。
1.2 为什么“harness和agent的区别”会成为一个高频搜索问题
我在各个技术平台搜过一圈,发现搜索“harness和agent区别”的人真不少。原因也简单:市面上主流的Agent框架中,LangChain、LangGraph、CrewAI、AutoGen这类库往往只提供一个“Agent对象”给你直接调用。文档里写agent = create_agent(...),很多新手就会误以为“这个agent就是全部了”。但实际上,这个agent对象是框架在内存里帮你装好的壳,它的背后还挂着一大堆Prompt模板、工具注册表、状态定义、模型绑定。这个“壳”就是harness的产物,而不是agent本体。
更麻烦的是,框架本身经常把Harness和Runtime混在一个包里。你调用LangGraph的app.invoke()时,它既做图编排,又偷偷替你做了一部分运行时该做的事,比如把状态存到checkpointer里。于是你在本地开发时感觉两者就是同一个东西,一旦上了生产,并发一高、任务一长、进程一重启,Runtime缺失的问题就全暴露了。所以本文花大篇幅讲清“harness和runtime的分界”,远比单纯区隔“harness和agent”更有用。
1.3 用三句话建立整篇文章的认知框架
为了便于后续阅读,可以先记住三个短句。
- Agent是你要交付的业务实体,负责“该做什么、什么时候做”。
- Agent Harness是组装Agent的工程骨架,负责“用什么模块、按什么顺序、怎么把模型和工具焊在一起”。
- Agent Runtime是承载Agent运行的基础设施,负责“进程活了没、状态碎了没、并发挤不挤、日志丢了没”。
这个分层不是理论洁癖,而是工程上必须有的分工。没有harness,Agent的复杂度会淹没在重复代码里;没有runtime,Agent根本扛不住生产环境的真实流量。接下来我分别把这两层拆开看。
2. 拆解Agent Harness:它负责Agent“怎么被编出来”
2.1 Harness到底包含哪些常见模块
Agent Harness这个名字,英文原意是“线束”或“夹具”,类比到Agent领域很贴切:它像电工用的线束一样,把模型、记忆、工具、策略这些零件按图纸捆在一起,形成一台可以通电的设备。注意,这个阶段关注的是“装配”,不是“通电运转”。
一个典型的Harness框架,至少包含下面这些能力。
- Agent对象的抽象:统一封装“模型调用+工具选择+意图判断”的入口,让你不需要每次手写循环。
- 工作流编排模型:状态机、图结构、顺序Pipeline或基于角色的多Agent对话流程,用来表达“先做什么后做什么”。
- 工具注册体系:把外部API、数据库操作、代码解释器作为可被模型调用的工具声明出来,并管理参数schema。
- Prompt与上下文组装:把系统提示词、历史对话、工具说明、用户输入拼装成最终请求上下文的逻辑。
- 输出解析与Schema校验:让模型输出走结构化协议,而不是直接吐一段文本。
- 测试与仿真环境:把各种上下文注入Agent,模拟不同分支,验证编排逻辑是否符合预期。
可以这样理解:Harness是你在写代码阶段就能感知到的一切“约束性框架”。比如你定义一个ReAct循环,本质上就是在说“模型推理一次,如需工具就调用,观察结果后再推理”。这个循环本身不会平白无故出现,是harness在结构上帮你搭好并限制了循环边界。
2.2 用LangGraph的一段代码理解“装配”的实感
目前开源社区里,LangGraph是把Harness层体现得最明显的一个。它用图结构定义Agent的编排:你声明节点、声明边、声明状态类型,最后调用compile()把它变成可执行对象。比如下面这个非常简化的流程:
from langgraph.graph import StateGraph, START, END from typing import TypedDict class AgentState(TypedDict): question: str intermediate_steps: list def call_model(state: AgentState): # 实际工程里这里会调用LLM,并把结果记录到state return {"intermediate_steps": [("call_model", state["question"])]} def execute_tool(state: AgentState): # 这里根据模型决定调哪个工具 return {"intermediate_steps": state["intermediate_steps"] + [("execute_tool", "ok")]} graph = StateGraph(AgentState) graph.add_node("reason", call_model) graph.add_node("act", execute_tool) graph.add_edge("reason", "act") graph.add_edge("act", "reason") # 循环直到条件满足 graph.add_edge(START, "reason") app = graph.compile()看到没有,这个app对象就是一个被装配好的Agent程序。你定义的图、状态、工具入口,都是harness层面的东西。它本身不关心进程崩溃后怎么恢复、多个请求并发时状态会不会串、请求量大了要不要扩机器——这些都是Runtime的事。但框架把图定义和运行封装到了一起,很多人因此误以为app就是完整系统。其实,它只是一个等待被运行的“程序包”。
2.3 Harness设计得好不好,直接影响后续Runtime的稳定性
这一点特别容易被忽略。很多团队先把Harness选型定了,Agent在本地跑得很欢,结果部署上线后惨不忍睹。为什么?因为Harness里写死的很多假设,一到Runtime就成了灾难。
举几个真实感受。
第一,如果Harness在Graph节点里直接用全局变量或内存List存状态,那这个Agent在单进程单请求下没问题,但在Runtime里一旦并发,状态就会互相污染。正确做法是让状态数据流式地记录在状态对象里,并把状态对象交给Runtime的checkpoint机制管理。
第二,如果Harness层把“重试”“退避”逻辑写在工具函数内部,而不是交给Runtime的调用链路,那一旦工具超时,你会发现重复调用和请求熔断混在一起,很难排查。工具本身应该尽量“傻一点”,把可靠性交给专门的运行设施去管。
第三,Harness的平台绑定不要做太死。比如你把LangGraph的StateGraph对象直接塞进业务数据库存起来,后续版本升级会让你很难受。编译好的图对象应该是可重建的,不要随便序列化持久化。
实际经验是:把Harness当作“前端工程”来治理。频繁变更、依赖重构、要做版本管理,还要有回归测试。每次改Prompt模板或图结构,都当成一次发版,这样后续的Runtime才不会因为harness被塞进一堆脏逻辑而崩溃。
3. 拆解Agent Runtime:它负责Agent“怎么被跑起来”
3.1 Runtime在Agent每次干活时具体做了什么
如果Harness是工厂里装配机器的流水线图纸,那么Runtime就是给已经造好的机器通电、供料、做安全监控的厂房系统。没有厂房和电力系统,图纸再漂亮也只是纸面功夫。
Agent Runtime在真实运行时要扛起这几类职责。
- 生命周期管理:创建Agent运行实例,安排它执行,超时或异常时安全终止;对长时间运行的Agent,还可能要支持暂停、恢复。
- 并发调度与队列:多个用户同时触发Agent时,要决定谁先谁后,哪些任务需要排队,哪些任务可以并行。如果Agent内部有循环,还要防止死循环把资源耗尽。
- 状态持久化与会话恢复:每一轮对话、每一步中间结果、工具返回值,都要能落到持久化存储。进程挂了或者发版重启后,能根据会话ID把Agent恢复到之前的状态。
- 模型接入与网关:统一处理大模型API的调用、鉴权、限流、重试、超时,以及Token用量统计和成本控制。
- 工具执行沙箱:很多Agent会调用代码解释器、浏览器、文件系统。这些工具如果和Runtime主进程共享权限,一旦Prompt注入,后果非常严重。所以成熟Runtime会把工具放到隔离沙箱里执行。
- 可观测性与追踪:把每一次模型的输入输出、思考链路、工具调用记录保存下来,方便调试和事后审计。
- 安全与配额:限制单个会话Token消耗、限制工具执行时长、做敏感信息脱敏。
你可能会觉得,这不就是一个后端服务该做的事吗?对,本质上Agent Runtime就是一个为“带有推理循环和无固定输入顺序”的AI任务设计的后端运行时。它和普通Web后端最大的区别在于:Web请求通常是一次性的短请求,而Agent任务可能有几十轮内部循环,可能调用多个外部系统,还可能需要几分钟甚至几小时才能完成。这导致Runtime必须对任务状态、分布式追踪和恢复能力提出更高要求。
3.2 拿一个“天气查询助手”走一遍Runtime链路
为了更有体感,我举一个最简单的天气Agent例子,它内部只做一件事:根据用户问题判断要不要调天气API,需要就调用,再整理结果返回。
当用户请求打过来时,Runtime一侧实际会发生这些事。
第一,接入层收到请求并完成鉴权。它根据对话ID去持久层读取会话状态,如果这是一个新会话,则初始化新的Agent状态。这里如果没有Runtime恢复机制,那么每次请求都只能从零开始,多轮对话就无从谈起。
第二,执行引擎把恢复出来的状态交给编译好的Graph(也就是Harness产物)开始跑第一个节点。模型调用不是直接裸连API,而是经过一个统一的LLM网关,由网关做超时控制和重试。假如第一个节点因为模型API网络抖动失败了,Runtime会判断该节点是否可重试,并根据退避策略等待片刻再次调用。
第三,图跑到“调天气工具”这一步时,Runtime会把工具调用请求发到隔离的执行沙箱里。沙箱里没有主进程的数据库连接,只有被放行的HTTP出口。返回结果后,Runtime将新状态写回Redis或Postgres,保证操作可恢复。如果进程这时突然崩溃,重启后能从数据库里找到当时执行到哪个节点,并继续未完成的部分。
第四,整轮执行结束后,Runtime把最终消息返回用户,同时把这次完整调用链路(模型调用次数、Token消耗、工具耗时、费用估算)写入观测平台。
这件链路里,哪一步是靠Harness实现的?只有“状态类型、节点函数、边的跳转逻辑”。剩下的大量工作——恢复、网关、沙箱、持久化、观测——全是Runtime的职责。所以当你把一个Agent Demo变成一个线上服务时,你真正要搭建和运维的,其实就是这套Runtime。
3.3 自建Runtime时,你可以参考哪些组件
不是每个团队都需要从零写Runtime。市面已经有很多成熟组件,关键是按职责把它们组合起来。我这里给一套个人常用的“最小可生产”组合。
| 功能 | 可选实现 | 为什么这么选 |
|---|---|---|
| LLM接入网关 | LiteLLM、Portkey、OneAPI | 统一多家模型API,负责限流、重试、密钥管理 |
| 执行编排引擎 | Temporal、LangGraph Platform | 长时任务需要可靠恢复,Temporal擅长持久化工作流状态 |
| 状态保存 | Redis + PostgreSQL | Redis用于热会话,Postgres负责冷存储与审计,组合性价比高 |
| 工具沙箱 | gVisor、Firecracker、Docker内运行受限程序 | 隔离不可信的工具代码,避免工具逃逸影响主服务 |
| 可观测追踪 | Langfuse、LangSmith + OpenTelemetry | 追踪模型思考链路和Token消耗,生产排查必备 |
| 任务队列 | Redis Stream、RabbitMQ、Kafka | Http接口外的异步触发和批处理任务需要队列缓冲 |
如果团队小、不想自己运维,直接使用LangGraph Platform这类托管Runtime是非常省心的选择。它已经把checkpointer、服务接口、后台任务和观测做进一个托管平台里,你只需要上传编译好的Harness产物。缺点就是平台绑定,后续如果想换更大的执行引擎,迁移成本偏高。对大多数还在验证业务阶段的团队来说,能用托管就先托管,把时间留给业务本身。
4. Agent Harness和Agent Runtime的九大区别图解
4.1 一张表看透所有关键差异
为了彻底把两者分开,我列过一张对比表,几乎每个项目都有人找我讨要。这里直接放出来。
| 对比维度 | Agent Harness | Agent Runtime |
|---|---|---|
| 发生阶段 | 开发、装配、编译期 | 部署、调度、运行期 |
| 面向对象 | 应用开发者 | 平台工程师、运维、SRE |
| 核心问题 | 怎么把模型/工具/流程组织成Agent | 怎么让Agent在长时间、高并发下稳定工作 |
| 典型产物 | 编译后的Graph对象、可注册的工具集 | 服务、执行器、分布式任务队列 |
| 修改代价 | 改完要联调测试、发版 | 改完要重启服务、可能影响在线任务 |
| 故障示例 | 意图判断错误、工具调用参数错、路由死循环 | 进程OOM、会话状态丢失、API限流、上下文串号 |
| 关键指标 | 准确率、召回率、编排可理解性 | 可用性、P95延迟、恢复时间RTO、成本 |
| 代表实现 | LangGraph、CrewAI、Semantic Kernel、AutoGen | LangGraph Server、Temporal、Kubernetes、自研AgentService |
| 主要日志诉求 | 节点输入输出、Prompt内容、工具参数 | 资源监控、链路追踪、错误堆栈、告警 |
看这张表,你会发现Harness关心的问题大多是“这一轮该调用哪个函数”,Runtime关心的问题是“这个服务现在还能不能扛得住”。如果遇到问题第一反应是去看Prompt或流程编排,说明你在查Harness层;如果第一反应是看CPU、看日志、查数据库链接,说明你在查Runtime层。
4.2 四个最常被问错的实际场景
很多人即便看了表,到具体场景里还是会判断失误。我整理过四个高频问题,你可以拿来自测。
场景一:Agent在线上回答一半突然中断,恢复后却忘记之前已经做过哪几步。很多人会去检查Harness里的状态定义和图节点,实际上这个问题的根源在Runtime缺了持久化。只要把状态保存加上,Graph本身一行都不用改。
场景二:我改了Prompt,也重新reload了代码,但线上Agent还是按老Prompt回答问题。这不是Runtime问题,而是harness产物没重新构建发布。不少平台会把编译结果缓存到特定版本,你没有生成新版本,线上自然不变。记住,改harness一定走完整的版本发布流程,重启进程不等于发布新图。
场景三:同一套Agent,本地跑一次没问题,线上并发一高就频繁出现工具调用串了。这时候不要怀疑Prompt写得不对。真要查,通常问题在Runtime的任务调度:你为了性能把一个进程内的共享状态对象暴露给了多个会话,互相踩踏了。需要对每个会话做隔离,或者在Runtime层为每个请求分配独立的状态上下文。
场景四:Agent明明没有主动运行,却过一段时间自己开始调用工具。这也不是Harness的编排逻辑问题,而是Runtime的定时触发或后台任务在工作。如果你没配置权限,可能是平台默认开启了某种长时Agent的自动恢复。这类情况要去Runtime的触发配置里查,别去翻代码里的Graph定义。
4.3 真正的选型建议:先拆层,再选型
我见过不少团队在选型时直接把LangGraph全家桶一套上,结果发现Graph层很满意,Runtime层又觉得不够用,最后不得不换。我建议先拆层再选型。
如果你只是做原型验证、跑一个离线脚本,那LangGraph的库本身够用,Harness层和Runtime层都挤在一个进程里,无所谓。
如果你要做线上API服务,多用户并发调用,优先确认框架是否提供独立Runtime服务。如果没有,要么自己写一层服务来管理会话和并发,要么把任务状态交给Redis/Postgres,不要裸用库里的内存状态。
如果你要做长时间运行、几十步以上的复杂任务,Harness用什么其实没那么关键,重要的是Runtime的任务队列和执行引擎能不能在服务重启后恢复任务。这种场景我建议直接调研Temporal这类分布式工作流引擎,它所提供的可靠执行能力,正是Agent Long-running任务的解药。
如果你的核心场景是对外部工具高度依赖、安全要求高,那么Runtime的沙箱设计就是第一优先级。Harness流程再精巧,工具一旦逃逸,整个系统都不可信了,所以要把精力放到隔离方案上。
5. 实操中我踩过的坑,以及一套排障速查清单
5.1 三个真实事故复盘
我过去一年为好几个Agent项目做过架构和排障,这里挑三个印象最深的记录一下,给后来者提个醒。
第一个事故发生在一次图升级后。开发同学在Harness里调整了状态机的边,让Agent在“用户拒答”后走澄清节点而不是直接结束。本地测试清晰正确,但上线后总是出现Agent不停地追问,行为跟本地版本完全对不上。排查了大半天,最后发现是平台把编译后的Graph服务按“会话ID”做了缓存,老会话仍然用旧版本图继续跑。根因不在Harness逻辑,而在Runtime侧的版本切换策略。从那以后,我们养成了一个习惯:大版本改图时,要么弃用所有历史会话,要么为会话版本号加迁移逻辑。
第二个事故和沙箱权限相关。我们的Agent有一个工具是读取上传的Excel文件。在本地直接跑文件读写没问题,一上开发环境,工具老报“Permission denied”。当时开发第一反应是harness里的工具函数出了问题,反复检查代码没发现异常。后来排查发现,环境的工具执行沙箱默认只开放了临时目录,而文件上传落盘到了另一个挂载点,沙箱内根本访问不到。这就是很典型的Runtime环境和Harness逻辑边界不清导致的排查空转。建议所有工具函数先声明自己需要哪些外部资源,然后由Runtime侧统一配置权限,不要指望工具自己解决环境问题。
第三个事故更隐蔽。我们给会话状态做的持久化键只用了简单的对话ID,结果不同渠道来源的同一个对话ID发生碰撞,A用户在某一步的工具返回值被B用户下一个请求读到,出现了严重的串号。问题出在Runtime层的状态存储设计,而Harness完全感知不到。后来我们把持久化键改成了“渠道+用户+会话”三段式,并加了渠道隔离,才彻底解决。这类问题特别容易发生在你刚开始并发上线Agent的时候,是Runtime设计里最值得提前考虑的一部分。
5.2 遇到问题时,先判断该查哪一层
结合前面的踩坑经验,我整理了一套速查思路,遇到问题可以按这个顺序判断。
| 现象 | 优先排查Harness还是Runtime | 排查要点 |
|---|---|---|
| 回答内容不符合预期 | Harness | Prompt、模型参数、图节点逻辑、工具选择 |
| 请求时好时坏,间歇性超时 | Runtime | 限流、网关重试、Pod资源、网络账号余额 |
| 多轮对话记忆丢失 | Runtime | 持久化组件、会话ID、checkpointer配置 |
| 老会话还在用旧逻辑 | Harness和Runtime都查 | 是否重新编译发版,历史会话有没有指定版本 |
| 工具调用顺序乱了 | Harness | 状态机、边条件、上下文更新逻辑 |
| 工具执行失败但代码正确 | Runtime | 沙箱权限、网络策略、依赖库版本、环境变量 |
| 进程启动后不响应 | Runtime | CPU/内存/DB连接数、初始化加载是否有死锁 |
| 异常堆栈指向模型API | Runtime | 网关密钥、超时参数、模型限流配额 |
这里有个通用原则:凡是“单一输入下行为一定”的问题,先查Harness;凡是“输入一样但行为偶尔不一致、跟并发和资源有关”的问题,多半在Runtime。判断时还要注意,很多深度链路的故障其实是两层共同作用的结果,不建议只盯一层。
5.3 给团队分工和代码组织的一点建议
最后说说团队分工。一个Agent产品团队,至少要有两种角色视角:一个是Agent开发,负责Harness层的Prompt、Graph、工具设计与质量;一个是平台或后端负责人,负责Runtime层的服务治理、状态底座、沙箱和观测。如果一个人同时扛两层,很容易在排查问题时只盯着自己熟悉的层,忽略另一层的风险。
代码仓库也建议分清楚。Harness相关的代码库可以叫agent-definitions,里面只放图、节点、工具注册。Runtime相关的库叫agent-runtime或agent-service,里面只放服务、环境配置、存储、部署脚本。两层之间通过明确的接口或镜像发布物连接。这样当线上出问题时,团队第一反应就能按代码归属定位到正确方向。
另外,日志规范也可以分层。Harness层打的是结构化业务日志:哪一轮、哪个节点、哪个工具、模型输入输出摘要。Runtime层打的是系统日志:容器重启、内存回收、数据库超时、API调用失败。两层日志用同一个traceId串起来,排障效率会高非常多。这是我们在经历数次事故后沉淀下来的最大经验。
很多项目最终失败,不是输在模型能力不足,而是输在环境一复杂就分不清到底是Agent“没想对”还是“没跑稳”。把Harness和Runtime的边界钉在心里,你排查问题的思路就会清晰很多。选型时也记住一点:Harness可以追新,但Runtime尽量走成熟方案。实际操作中,我对Harness的改动一定谨慎发版,对Runtime的扩容一定提前预判,这样做了之后,Agent系统的稳定性明显比早期“一锅炖”的时候要好上不少。