去年接了个内部知识库问答 Agent 的项目,技术方案看着挺简单:模型 + 提示词 + 工具调用,跑一个循环不就完了?结果一上生产就翻车:Token 烧得飞快,工具调用乱套,日志里全是重复请求,出问题了都不知道是哪一轮哪次调用搞的。复盘下来,问题出在我把 Agent 工程当成了一条单线程的"提示词循环",而生产环境的 Agent 本质上是一台需要分层设计的机器。
真正能扛住线上压力的 Agent 工程,基本都围绕三个词展开:Harness、Loop、Graph。Harness 是控制台和骨架,Loop 是思考-行动的执行内核,Graph 是把多条路径编排成工作流的网络。这篇文章我打算把这层架构完整拆开,讲讲每一层到底解决什么问题、怎么落地实现、生产环境里怎么排查优化。内容面向正在做 Agent 应用开发的工程师、准备把 Agent 部署到内网的运维同学,也适合那些被框架文档绕晕了、想从原理层面理解 Agent 项目的朋友。
1. 先想清楚:Harness、Loop、Graph 到底各管哪一层
很多 Agent 项目做不好,不是因为模型不行,而是因为所有逻辑都糊在一起。我见过不少代码,把上下文组装、工具执行、循环控制、日志记录全塞进一个 while 循环里,几百行代码搅成一锅粥。理解三层架构的第一步,是接受一个事实:Agent 应用的复杂度必须靠分层来消化。
1.1 我踩过的"单层 Agent"坑
最开始我做 Agent 的方式特别粗暴:写一个 while 循环,把用户问题、历史对话、工具返回全都往 messages 里塞,然后反复调模型接口。本地测试一切正常,一旦放到生产环境,问题全冒出来了。
第一个问题是上下文失控。工具返回的检索结果动辄几千字,模型没筛选就全读进去了,几轮下来上下文窗口直接打满,后面的回复质量断崖式下跌。第二个问题是工具调用的错没法定位。模型连续调了五次工具,第五次传参格式错了,到底该重试还是该纠正?代码里没有这个决策逻辑,只能报错退出。第三个问题是业务逻辑和模型逻辑搅在一起。有些问题根本不需要调工具,有些问题必须走人工审批,单靠循环里的提示词很难优雅实现这种分支控制。
这些坑的本质,就是把"模型推理"这一个环节当成了整个应用。实际上模型推理只是内核,它需要外部机制来约束、包装和编排。
1.2 三层架构各自的职责边界
用一张图很难讲清楚三者的关系,我用一个开车类比:Loop 是发动机的做功循环,负责"吸气-压缩-做功-排气"的持续运转;Harness 是驾驶舱里的仪表盘和方向盘,你通过它控制车速和方向,它负责监控油量、水温、转速这些运行参数;Graph 则是导航路线,决定这辆车从 A 到 B 经过哪些路口、在哪个路口转弯、遇到堵车怎么绕行。
落到 Agent 工程:
- Harness管的是"运行环境":上下文如何组装、工具如何注册和暴露、哪些操作需要安全审批、token 预算怎么控制、skill 怎么加载。它本身不参与模型推理,但决定了推理能否安全高效地发生。
- Loop管的是"执行核心":模型根据当前状态决定"是直接回答还是调用工具",调用工具后观察结果,再继续推理,直到产出最终答案。它是 Agent 最原始的运转单元。
- Graph管的是"流程编排":把多个 Loop、函数、人工节点组织成有向图,支持条件分支、并行执行、循环回退、人工审核。它解决的是一个 Loop 处理不了复杂流程的问题。
这三层不是互相替代的关系,而是层层包裹的关系。最内层是 Loop,外层套一个 Harness 管好输入输出和安全边界,最外层用 Graph 把这些 Loop 编排成满足业务需求的工作流。
1.3 这套架构到底适合谁
如果你只是写个 Demo 玩一玩,单循环 + 提示词完全够用,没必要上三层架构。但如果你是以下情况,我建议认真按分层思路来设计:
- 用 API 开发自动化任务或智能客服,需要稳定跑很久、成本可控
- 在做多 Agent 协作或复杂工作流,需要分支、并行、人工审批
- 要把 Agent 系统部署到内网服务器,模型、技能、插件都需要离线管理
- 已经开始用 LangGraph、Dify、Coze 之类框架,但想知道这些框架到底替你管了什么
理解了三层架构,你再去看各种 Agent 框架的文档,会瞬间通透很多,因为框架的组件基本都落在某一层上。
2. Harness:把 Agent 管控起来的驾驶舱
Harness 这个词在机器学习圈子里本来有"测试夹具"的意思,在 Agent 工程里可以理解为"包裹 Agent 的那层骨架"。它负责解决一个核心问题:模型接口谁都会调,但怎么调得稳、调得省、调得安全,全靠 Harness。
2.1 Harness 和 Agent 本身的区别
很多人分不清 Harness 和 Agent。简单说,Agent 是"模型 + 提示词 + 工具定义"组成的智能体,它具备推理和行动的能力;Harness 是套在 Agent 外面的那层管控机制,它不产出智慧,只保证智慧被用在合适的地方。
我见过一个不错的表述:Agent 是一台发动机,Harness 是发动机舱、冷却系统、燃油控制系统和仪表盘的总和。没有 Harness,发动机也能转,但转多久、会不会过热、油耗多少、出了故障怎么诊断,全都没人管。
具体来说,Harness 至少承担这几类职责:
- 上下文管理:system prompt 放在什么位置、历史对话怎么截断、工具返回的结果插入到哪个层级
- 工具注册与校验:Agent 能调用哪些工具、参数结构长什么样、调用前要不要权限检查
- 策略控制:单轮 token 上限、总预算、最大调用次数、敏感操作是否需要人工审批
- 可观测性:记录每次请求的输入输出摘要、工具调用参数、耗时、成本
- 技能加载:按需挂载某个领域的提示词片段、工具集和辅助脚本
2.2 动手写一个最小 Harness
这里我用 Python 写一个最小的 Harness 骨架,便于理解它的内部结构。实际生产环境你可能用现成框架,但原理是一样的:
class Harness: def __init__(self, model_client, system_prompt, tools): self.model_client = model_client self.system_prompt = system_prompt self.tool_registry = {t.name: t for t in tools} self.context_manager = ContextManager(max_tokens=30000) self.policy = Policy(max_iterations=10, max_total_tokens=50000) self.observer = Observer() def build_context(self, user_input, state): messages = [{"role": "system", "content": self.system_prompt}] history = self.context_manager.trim_history(state.history) messages.extend(history) messages.append({"role": "user", "content": user_input}) # 关键:把工具调用中间结果也纳入上下文,但要压缩 if state.last_observation: messages.append({ "role": "user", "content": f"[工具返回摘要] {self.context_manager.compress(state.last_observation)}" }) return messages def execute_tool(self, name, params, user_context): if not self.policy.check_permission(name, user_context): raise PermissionError(f"tool {name} not allowed") tool = self.tool_registry.get(name) if not tool: return {"error": f"unknown tool {name}"} # 参数校验在这里做,不要把脏参数直接交给工具 validated = tool.validate_schema(params) return tool.run(**validated)你可能发现了,这个 Harness 自身的run方法还没写。因为它只负责提供执行环境,真正的循环逻辑要放在 Loop 那一层。这种分离非常重要,它让你可以独立替换任何一层——换个模型、加个工具、改个上下文策略,都不需要动其他代码。
2.3 Harness 的安装与 Skill 部署实践
Harness 类的组件比较多,实际项目中建议用扩展机制来组织,不要全塞在一个类里。很多开源 harness 工具都支持插件和 skill 目录,比如社区里常见的目录结构:
harness/ ├── config.yaml # 模型配置、预算、工具白名单 ├── skills/ │ ├── sql_analysis/ │ │ ├── SKILL.md # 技能说明,会注入 system prompt │ │ └── run.py # 技能附带脚本 │ └── meeting_summary/ │ ├── SKILL.md │ └── templates/ ├── plugins/ # 插件,如日志、缓存、安全策略 └── main.pySkill 的部署其实比很多人想的要简单。一个 skill 的本质就是一组提示词片段 + 可执行脚本 + 元信息描述,harness 启动时扫描 skill 目录,把 SKILL.md 的内容注入到 system prompt,同时把脚本注册成工具。这样不同团队负责不同 skill,互不干扰。
如果是内网服务器部署,思路也是清晰的:先在开发机把 skill 整理好,通过 Git 私服或打包工具分发到内网机器,harness 启动时从本地目录加载。模型服务同样可以内网化——用 Ollama、vLLM 这类工具部署开源模型,harness 里配置本地 API 地址即可。整个链路不依赖外网,部署完就能跑。
这里有个我踩过的坑:skill 提示词写得又长又泛,全塞进 system prompt,结果每轮请求都浪费几千 token。后来按"用的时候才加载"的思路,把 skill 的完整描述从 system prompt 里拿掉,只保留技能名称和触发条件,等到模型决定调用了再加载详细规则,成本立刻降了一截。
3. Loop:驱动 Agent 思考与行动的引擎
Loop 是 Agent 最核心的执行单元。如果说 Harness 是驾驶舱,Loop 就是发动机本身。它的运转逻辑直接决定了 Agent 能不能通过工具使用解决复杂问题。
3.1 循环的底层逻辑:ReAct 模式
Loop 最常见的模式是 ReAct,也就是 Reasoning + Acting + Observation 的交替循环。这个模式相信大家不陌生:模型先推理,决定下一步动作,要么直接给出最终答案,要么调用某个工具;工具返回结果后,模型基于这个观察继续推理,直到最终答案出现。
用伪代码表示:
while iteration < max_iterations: response = model.generate(messages) if response.is_final_answer: return response.answer if response.has_tool_call: observation = harness.execute_tool(response.tool_call) messages.append(observation) iteration += 1 else: # 模型既没给最终答案,也没调工具,说明陷入了死胡同 messages.append({"role": "user", "content": "请基于已有信息继续思考并给出答案"}) iteration += 1ReAct 的变体也很多。比如 Plan-and-Execute 模式会先让模型生成一个完整的行动计划,然后逐步执行;Reflexion 模式会在执行完后让模型反思自己的错误,带着反思结果再来一轮。选哪种模式取决于任务的复杂度——简单问答用 ReAct 就够,多步骤任务建议 Plan-and-Execute,需要不断自我纠错的任务可以上 Reflexion。
3.2 循环的跳出条件与保护机制
Loop 最容易犯的错误是"永远循环下去"。生产环境必须有明确的跳出条件,最基础的保护有这几条:
- 最大迭代次数:一般设 5 到 10 次。超过上限还没给出答案,直接走兜底逻辑,比如返回提示"问题过于复杂,请重新描述"。
- Token 预算:每次循环都会消耗上下文空间,一定要在 Harness 层记录累计消耗,接近阈值时强制结束。
- 工具连续失败熔断:同一工具连续报错达到 3 次,说明要么工具本身有问题,要么模型理解错了参数,此时应该把错误信息反馈给模型,而不是继续重试。
- 超时控制:单轮模型调用超时、工具调用超时,都要单独处理,避免整个请求被拖死。
这些保护机制为什么重要?我见过一个案例,Agent 在查数据库时总是把 SQL 写错,模型一次又一次重试,每次重试都会把上一次的报错信息追加到上下文,最后上下文里堆了 7 条错误信息,模型反而被错误带偏了。加了熔断机制之后,连续失败会触发"换一种方式查询"或"请人工介入"的分支,整个系统的稳定性明显提升。
3.3 Loop 里最容易翻车的三个问题
Loop 虽然只有几行代码,但生产环境运行起来,翻车点远比想象的多。
第一个是工具返回格式不合法。模型调工具时传参偶尔会不符合 JSON Schema,直接执行会报错。我的做法是在 Harness 层做严格校验,校验失败时把校验错误返回给模型,并明确提示"请根据以下 schema 重新生成工具参数"。大多数情况下模型会在下一轮修正。
第二个是模型把工具输出当成交付结果。比如检索工具返回了一段资料,模型不总结直接原样输出给用户,回答看起来像文档复读机。解决方法是 system prompt 里强制区分"工具观察"和"最终回答",还可以加一个输出解析层,检测回答是否包含工具原文。
第三个是状态序列化时的循环引用问题。很多 Agent 框架会把状态里的对象直接序列化存储或传输,如果状态对象里有互相引用的字段,某些序列化库会直接抛错——类似self referencing loop detected这种报错就很典型。我之前在调试一个带内存模块的 Agent 时遇到过,排查半天发现是 memory 对象里存了一个全局上下文引用,导致序列化递归无限循环。解决方式很简单:序列化前做深拷贝,或者只提取基础字段,不要序列化复杂对象引用。
4. Graph:从单条循环到可编排工作流
Loop 能解决单任务的问题,但真实业务往往是多步骤、多分支的。客服 Agent 要能判断问题类型,是咨询、投诉还是退款;研发助手要能区分查询代码和修改代码;审批流程需要人工节点介入。这些场景,单条 Loop 根本组织不出来,需要 Graph。
4.1 为什么单条 Loop 不够
单条 Loop 的本质是一个线性的"推理-行动"循环,它没有显式的流程边界。你可以在提示词里写"如果用户要退款,请跳到退款流程",但跳出之后怎么回来、中间结果怎么保存、人工审批的节点放哪里,全都依赖模型自觉。模型一旦"忘记"流程,整条链路就断了。
Graph 把流程显式化了:每个步骤是一个节点,节点之间用边连接,边可以带条件。这样做的好处是流程可控制、可观测、可维护。你可以在图里看到一个请求走了哪些节点、在哪个节点被卡住,也可以在任意节点插入人工审核、告警、日志。
4.2 图编排的三要素:节点、边、状态
理解 Graph 只需抓住三样东西:
- 节点(Node):执行单元,可以是一个完整的 Loop、一个普通函数、一个 HTTP 请求、一个人工审核界面。节点是可组合的积木。
- 边(Edge):节点之间的连接,分顺序边和条件边。条件边根据状态里的某个字段决定下一步走向。
- 状态(State):所有节点共享的上下文对象,存用户输入、中间结果、工具返回、标志位。每个节点从状态里读它需要的东西,然后往里写新的东西。
我一直觉得状态是 Graph 设计里最容易被忽略、却又最关键的部分。打个比方,状态就是一个公共抽屉柜,每个工位的人往里面放自己加工完的半成品,下一个工位再从柜子里拿。如果抽屉里的东西没有规范的命名和结构,整个流程很快就会乱。
4.3 一个"检索-反思-回复"图的实现方案
假设我们要做一个知识库问答 Agent,需求是:简单问题直接回答,复杂问题先检索,检索结果不确定要重新检索,最终回答前还可以人工复核。用 Graph 来组织,大致这样:
- 分类节点:读取用户问题,判断是否需要检索。不需要检索的直接走生成节点。
- 检索节点:调用检索工具,把结果写入状态。
- 反思节点:判断检索结果是否覆盖了问题要点。覆盖不足就回到检索节点,并注入"上一轮结果不够好,请换关键词"的提示。
- 生成节点:基于检索结果和对话历史,生成最终回答。
- 人工复核节点:可选,高风险问题走人工审核后再返回。
用代码表达的话,LangGraph 这类工具提供了现成的状态图模型,核心写法类似:
from langgraph.graph import StateGraph g = StateGraph(State) g.add_node("classify", classify_node) g.add_node("retrieve", retrieve_node) g.add_node("reflect", reflect_node) g.add_node("generate", generate_node) g.add_node("human_review", human_review_node) g.add_edge("classify", "generate", condition=lambda s: not s.need_retrieval) g.add_edge("classify", "retrieve", condition=lambda s: s.need_retrieval) g.add_edge("retrieve", "reflect") g.add_edge("reflect", "retrieve", condition=lambda s: not s.retrieval_good) g.add_edge("reflect", "generate", condition=lambda s: s.retrieval_good) g.add_edge("generate", "human_review", condition=lambda s: s.need_review)这里"反思节点"是我后来加进去的。最初版本是检索完直接生成,结果经常出现检索结果与问题完全无关,模型却硬撑着回答。反思节点能显著提升回答质量,代价是多一次模型调用,换来的准确性很值。
Graph 的另一个优势是并行编排。比如要整理一份竞品分析报告,可以同时跑三个检索节点,分别查产品、查价格、查用户评价,最后汇总。在 Loop 里这是串行任务,在 Graph 里可以配置并行执行,响应时间能减少一半以上。
5. 生产环境硬仗:预算、观测、安全与问题排查
架构搭清楚了,代码也写完了,真正的考验在生产。这一节我把上线后最常遇到的问题集中梳理一遍,每一条都是真金白银换来的教训。
5.1 Token 预算怎么算、怎么压
Agent 项目的成本大头就是 Token,尤其是检索类 Agent,工具返回的内容往往能把上下文窗口撑爆。我的经验是,上下文分配需要按固定比例规划,而不是等报错了再处理。
以 128k 窗口为例,我的分配参考:
| 部分 | 预算占比 | 说明 |
|---|---|---|
| 系统提示词 | 5k~8k | 固定开销,技能描述要精简 |
| 对话历史 | 16k~24k | 用滑动窗口,只保留最近几轮 |
| 工具返回/知识片段 | 32k | 压缩到相关段落,删除冗余 |
| 模型输出预留 | 4k~8k | 给最终回答留出空间 |
实际压 Token 有三个有效手段。第一是工具返回压缩,很多检索结果只保留前几百字加命中的关键段落,足够了。第二是历史会话总结,旧的对话定期让模型生成摘要,替换原始内容,保留信息密度。第三是减小 skill 的常驻描述,前面也提过,技能完整说明等触发时再加载。
量化手段也很简单:在观察层记录每次请求的 prompt_tokens 和 completion_tokens,按业务线聚合,一眼就能看出哪个 agent、哪个工具在烧钱。
5.2 可观测性:日志、追踪与状态快照
Agent 应用调试难度比普通接口高得多,因为中间状态多、不确定性大。没有好的观测手段,出了问题就只能瞎猜。
我最低限度的观测配置是这三个:每次循环都记一条 JSONL 日志,包含 trace_id、模型请求摘要、工具名、参数、返回摘要、耗时、token 数;全链路追踪,前端发起请求时生成一个 trace_id,所有相关日志和调用都带上它;状态快照,在 Graph 每个节点执行前后记录 state 的序列化副本。有了这三个,任何问题都能通过 trace_id 串起来,很快定位到是模型理解错了、工具调用错了,还是上下文出了问题。
状态快照有个坑,就是前面提到过的循环引用。记录快照时一定要做深拷贝,或者干脆只记录白名单字段。我见过有人把 state 直接打进日志,结果日志系统自己崩了。
5.3 安全边界与工具权限
Agent 能调工具之后,安全就成了硬约束。我的原则是"最小权限 + 白名单 + 参数校验"三管齐下。
工具注册只暴露必要的那几个,不要给 Agent 一个万能 shell。需要调用外部系统时,在 Harness 层按用户上下文做权限判断,普通用户不能触发删除类操作。参数一律按 JSON Schema 校验,模型传给工具的字符串参数尤其要小心,防止命令注入。高风险操作设计成"人工确认"节点,Agent 只能提交申请,不能直接执行。
内网部署场景下,还要额外注意依赖和安全策略的收敛。把 harness 运行所需的模型、知识库、技能文件全部放到内网环境,关闭不必要的出网请求,降低暴露面。还有一点容易被忽视:Agent 的工具会产生副作用,比如写入数据库、发送邮件,这些操作必须记录操作者 trace_id,方便审计。
5.4 常见问题排查实录
下面是生产环境最常见的几个问题,我整理了排查思路:
| 问题现象 | 可能原因 | 排查方式 | 解决方向 |
|---|---|---|---|
| Agent 反复调用同一个工具 | 上下文缺少"已经尝试过"的信息 | 看日志中工具调用历史 | 首次调用时在状态里记录,失败后提示换一种方式 |
| 回答里混入工具返回原文 | 提示词没有区分观察和最终回答 | 对比工具返回和输出片段 | 强化 system prompt,加输出解析层 |
| 每次运行结果差异很大 | 模型温度高、上下文截断策略不一致 | 对比同请求下的日志 | 固定参数,统一上下文截断窗口 |
| 响应延迟过高 | 串行调用过多 | 看链路耗时分布 | 把独立节点改成并行执行 |
| 序列化报循环引用 | state 里有互相引用的对象 | 检查序列化堆栈 | 深拷贝或白名单字段 |
| 工具报错后问题解决不了 | 模型被错误历史带偏 | 看失败前的上下文 | 加连续失败熔断和重试策略 |
5.5 最后再分享一个小技巧
我最近的习惯是,把所有 Harness 和 Loop 的可调参数全部外部化成一个 YAML 配置文件,包括模型名称、温度、最大迭代次数、工具白名单、上下文窗口分配比例。这样每次调参不需要改代码,改完重启就生效,测试不同配置的成本极低。对于 Graph,我还把每个节点的启用开关也放进配置里,哪天想跳过反思节点,直接关开关就行。
配置外部化之后,运营同学也能参与调优了,不用再追着开发改代码。这个经验,是我在一次紧急事故中总结出来的——当时为了调一个 Agent 的召回策略,我连续改了四次代码重新发版,后来把配置抽出来,十分钟就能完成一轮实验。
写在最后
做了几年 Agent 工程,我最大的体会是别急着上大而全的框架。先从 Loop 跑通最小闭环,再套 Harness 管好上下文和预算,等业务逻辑复杂到单条循环解决不了,再引入 Graph 编排。每一层都有它存在的理由,但只有在合适的复杂度下引入,它才是加分项而不是负担。
如果非要说一个最重要的建议,我会说:把"模型推理"和"工程控制"分开想。模型负责聪明,工程负责稳定。Harness 保证它不乱来,Loop 保证它运转,Graph 保证它走对路。这个思路清楚了,Agent 就不会再是玄学了。