☰
构建稳定AI Agent:Harness工程实战与FastAPI+LangGraph并发状态管理
2026/10/2 5:26:51 网站建设 项目流程

1. AI Agent 为什么总在“看起来能跑”和“一上生产就崩”之间反复横跳

这两年聊 AI Agent 的人越来越多,刷屏的 Demo 一个比一个惊艳:自动写代码、自动查资料、自动操作浏览器、自动做数据分析。但真正自己搭过 Agent、或者把 Agent 扔到生产环境跑过一阵子的人,心里多半都有个共同的困惑——这玩意在演示环境里什么都会,到了真实业务场景里怎么就什么都不敢保证?

我印象很深的一次经历:去年帮团队做一个内部知识库问答 Agent,单轮对话调模型、调 RAG 都挺顺利,demo 也给老板演示得风生水起。结果一接入团队群里几十个人同时用,各种问题就炸出来了:有人问着问着 Agent 突然开始胡言乱语,有人上传了一份 PDF 之后整个上下文直接“失忆”,还有人同一个问题连续问三次,三次拿到三种完全不一样的答案。那天下午我一边看日志一边抓头发,脑子里只有一个念头:只把模型能力堆起来根本不算做完了一个 Agent,真正的工程量在于让它在不可控环境里保持可控。

这就是所谓“构建稳定的 AI Agent”这件事的残酷真相:模型是不稳定的,工具是脆弱的,用户的输入是无规律的,而你要在这三堆不确定性之上,搭出一个尽量稳得住的生产系统。业界这几年逐渐把这一坨“让 Agent 稳定落地”的配套工程统称为Harness 工程——直白点说,就是给 Agent 套上的那套“缰绳”和“护栏”,包括了 Prompt 编排、工具契约、状态管理、错误恢复、可观测性这些东西。它不解决“模型聪不聪明”的问题,它解决的是“模型就算偶尔犯浑,系统也不能跟着崩”的问题。

这篇文章我就想围绕 Harness 工程这个核心话题,把我在实际搭建 Agent 过程中的思考和踩坑记录整理出来。内容不局限于某一个框架,但会重点结合 Python 生态里常见的FastAPI + LangChain + LangGraph组合来讲,最后也会聊到 Agent 中台化、产品化的方向。适合刚入坑 Agent 开发、想把原型变成生产级系统的同学参考。

2. AI Agent 稳定性问题的根源拆解

2.1 模型的不确定性是“原罪”,不是 bug

很多第一次搭 Agent 的人会习惯性地把 Agent 当普通接口来用:传一个 prompt 进去,期待一个 JSON 回来。但大模型本质上是个概率系统,同样的输入、同样的参数,每次生成的结果都可能有细微差异,遇到推理能力不足或上下文过长的情况下,差异会被放大到离谱的程度。

我见过一个真实的案例:某个 Agent 需要从用户输入里提取“城市”字段,开发者在 Prompt 里写得很清楚“只返回城市名,不要输出其他内容”,结果模型还是会偶尔多输出一句解释、或者把省份一起带出来。如果这个输出直接被代码拿去当变量用,后面的整个流程就全歪了。模型不是 bug,但它的概率属性决定了你不能用“确定性系统”的思维来构建上层逻辑。

处理这个问题的思路不是“让模型永远不出错”,而是让系统在模型出错时不至于全盘崩溃。Harness 工程里有一块核心工作就是“输出校验与结构化约束”:用函数调用(function calling)、JSON Schema 约束、输出校验器这些手段,把模型的自由输出关进笼子里。你可以允许模型偶尔胡说八道,但你的代码必须有办法一眼识别出它在胡说八道,然后触发重试或者兜底逻辑。

2.2 工具调用的脆弱性:Agent 的能力越大,爆炸半径越大

Agent 和普通 Chatbot 的本质区别在于它能调用工具——查数据库、调 API、读写文件、执行代码。能力边界扩大了,但随之而来的问题是:工具调用链越长,出问题的概率指数级上升。

举个最常见的例子:一个客服 Agent 先调用“查询订单接口”,拿到订单号之后调用“查询物流接口”,最后调用“生成回复模板”。三个环节里任何一个不通——订单接口超时、物流接口返回格式变了、生成模板时模型把参数搞错了——整个链路就挂了。更要命的是,模型本身并不保证“每一次都选对工具、填对参数”,它可能在第一步就把订单号填成用户 ID,让后面的所有操作都建立在错误前提上。

这就是为什么 Harness 工程里要反复强调“工具层隔离”和“契约测试”。你在让模型调用工具之前,必须先把工具自身的稳定性做起来:超时控制、重试机制、幂等设计、参数校验、异常返回约定,这些常规后端工程的手段,放到 Agent 的工具层一个都不能少。你不能默认模型会填对参数,你只能默认“模型填错参数是常态”,然后用校验逻辑把错误的调用挡在外面。

2.3 上下文管理与状态隔离:并发场景下最容易翻车的地方

标题热搜词里有一个很扎心的问题:AI Agent 怎么扛并发?很多人在本地跑单个 Agent 时完全感觉不到状态管理的压力,但一旦部署成多用户服务,问题立刻暴露。

核心原因是:大模型的对话上下文是“有状态”的,但大多数框架默认的用法却是“无状态”的。你把用户的对话历史存在内存里,来了一个请求就丢给模型,看起来没问题,但一旦多个用户同时访问,A 用户的上下文可能被 B 用户的请求覆盖,或者历史消息在并发写入时顺序错乱,模型就彻底“人格分裂”了。

我在实操中用 LangGraph 处理这个问题时发现,它的状态管理机制(StateGraph)天然支持显式定义状态对象和消息列表,只要把状态对象做对隔离,按会话 ID 区分不同用户的独立状态池,并保证每次更新是原子操作,绝大多数并发状态冲突问题都能在架构层面规避。核心原则就一句话:绝对不要把用户的上下文集放在一个全局变量里,哪怕你只是写个 Demo。

3. Harness 工程的核心机制拆解

3.1 定义“缰绳”:Harness 到底在约束什么

如果只用一句话概括 Harness 工程,那就是:在模型能力与业务目标之间,插入一层可控的“约束面”。它的职责范围包括但不限于:

  • 控制 Agent 的感知范围(该看什么上下文、不该看什么)
  • 控制 Agent 的行为边界(允许调用哪些工具,禁止调用哪些)
  • 控制 Agent 的输出形态(必须符合什么结构、什么格式)
  • 控制 Agent 的失败路径(出了错是重试、降级、还是终止)

这个思路和我之前做传统后端系统时的“契约优先”很像:你先定义好系统的边界和接口,再让实现去适配它。不同的是,传统系统的实现是代码,可控可测;Agent 世界的“实现”是模型,天然不可控,所以这个约束面不得不做得更厚、更严密。

实践中,我习惯把 Harness 拆成五个层次,从底层到顶层分别是:

  • 基础设施层:模型统一网关、模型降级路由、调用限额
  • 策略层:安全策略、权限策略、敏感信息过滤
  • 流程层:任务规划、步骤编排、状态流转
  • 工具层:工具注册、参数校验、结果校验、错误重试
  • 表达层:Prompt 模板、输出格式化、多轮上下文组装

每一层都是独立的模块,可以单独替换、单独测试。这样做的好处是:模型升级了只动基础设施层,业务逻辑变了只动流程层,工具接口变了只动工具层,互不干扰。

3.2 ReAct 循环与 Plan-and-Execute:两种主流的 Agent 运行时

构建 Agent 时最基础的决策是:你选择什么运行时模式来驱动 Agent 的“思考-行动”循环。

当前主流的有两条路线。一条是ReAct(Reason + Act):模型在每一步先“思考”,然后决定调用哪个工具,拿到工具结果后再思考下一步,循环往复,直到得到最终答案。这种模式适合任务路径不明确、需要动态决策的场景,但它的问题在于没有全局规划,每一步都是局部最优,遇到需要多步递进的复杂任务时容易绕圈。

另一条是Plan-and-Execute:Agent 先根据任务目标生成一个完整的执行计划(Plan),然后按部就班地执行每一步(Execute),在执行过程中根据实际结果动态调整计划。这种模式的结构性更强、更可控,也更适合生产环境——因为你可以把“计划生成”和“计划执行”两个环节分开监控、分开干预。

我在实际项目里的做法是两者混合:先让模型产出一个粗粒度规划(plan),再在每个规划步骤内部使用 ReAct 循环来动态决定具体操作。LangGraph 对这种混合模式的支持很友好,你可以在图里面定义不同的节点:规划节点负责产出步骤列表,执行节点负责调用工具,检查节点负责验证执行结果,决策节点负责判断计划是否需要调整。整个流程是显式写出来的流程图,而不是黑盒,哪里出了问题直接看状态就能定位。

3.3 状态管理:从“对话历史”升级为“业务状态机”

前面提到并发场景下的状态隔离,这里再深入扩展一下。传统 Chatbot 的状态管理只需要管“对话上下文”——用户说过的每一句话。但真正的 Agent 要做的是多步骤任务执行,它的状态远不止对话记录,还包括:

  • 当前任务目标及子目标分解情况
  • 已执行完成的工具调用及结果
  • 中间变量的值(比如“订单号已经提取出来了”)
  • 当前处于流程中的哪个阶段

这些信息如果只塞在一个“messages 数组”里,既不便于流程控制,也不便于故障排查。我的经验是把它升级成一个显式的业务状态对象,里面明确划分:goal(当前目标)、steps(规划步骤)、completed_steps(已完成步骤)、context(业务上下文)、messages(原始对话记录)。用 LangGraph 的话,这个对象就是传给 StateGraph 的 state,每个节点都能读取和更新它的字段。

这样做最大的好处是可恢复性。假设 Agent 执行到第三步调用数据库时超时了,如果状态对象还在,我可以直接从第三步重试,而不是把整个任务推倒重来。这在传统后端里叫“断点续传”,放到 Agent 世界里就是“任务可恢复”,是稳定性建设的关键一环。

3.4 可观测性:不仅看日志,还要看懂 Agent 的“心路历程”

传统后端排查问题看异常堆栈就够了,但 Agent 的问题往往没有异常——模型正常返回了,只是返回的内容是错的;工具正常调用了,只是调用的参数是错的。这时候你光看日志里有没有 error 是完全不够的,你必须要能看到模型的每一步输入输出、工具调用参数和返回值,才能在出错时还原现场。

所以我强烈建议在 Agent 的每一层都埋可观测性数据:

  • 模型层:完整 prompt、生成的原始输出、token 消耗、耗时
  • 工具层:调用参数、返回值、耗时、是否重试
  • 流程层:当前状态、当前节点、节点的输入输出

用 LangGraph 的时候,可以在每个节点上挂回调函数(callbacks),把节点执行信息吐到日志系统里。用 FastAPI 做服务层时,可以用中间件记录每个请求的完整链路 ID,把一次用户请求的所有日志串起来。关键是:可观测性不只是给人看的,更是给系统用的。当你积累到一定量的 Agent 运行日志后,你完全可以用规则或小模型自动识别“高失败率”的模式,提前预警。

3.5 护栏(Guardrails)与安全边界:宁可拒绝,不可乱来

Agent 一旦接入真实业务,就必须面对两个问题:它会接触到敏感数据,它可能做出危险行为。这已经不是技术问题,而是安全底线问题。

我的做法是在 Harness 里设三层护栏。第一层是输入护栏:用户输入进来先做敏感词过滤、越权检测、指令注入检测。第二层是行为护栏:Agent 要调用工具时,先校验这次调用是否在权限范围内、参数是否合法、目标资源是否越权。第三层是输出护栏:Agent 生成最终回复之前,先检查内容是否包含不该透露的内部信息。三层全过才放行,任一层不过就走兜底回复或者拒绝。

说实话,护栏这件事没有银弹,而是需要在具体场景里一点点补齐。但你可以在架构上一开始就留好位置,而不是等出事了才补,这是我在多个项目上的血泪教训。

4. 从原型到扛并发:基于 FastAPI + LangChain + LangGraph 的实战路径

4.1 技术选型为什么是 FastAPI + LangGraph,而不是别的

先回答一个常见问题:为什么我现在不用纯 LangChain 的 AgentExecutor,而转向了 LangGraph?原因很简单:LangChain 早期的 Agent 实现是个黑盒循环,你很难干预里面的步骤流转;LangGraph 把流程显式建模成图,给了你完全的掌控力。

这不是说 LangChain 不行,而是对于要上生产、要精细控制、要扛并发的项目来说,LangGraph 的“显式状态 + 显式流转”更符合 Harness 工程的要求。选 FastAPI 是因为它天然支持异步、性能好、生态成熟,拿来包一层 HTTP 服务面对前端或者内部系统调用都轻松。至于 LangChain,我现在主要用它已经沉淀好的工具封装和模型统一调用接口,把它当成一个轻量库来用,而不是让它来主导你的架构。

4.2 服务层设计:异步接口、请求级上下文隔离

服务层是整个系统的入口,它的核心职责是:把 HTTP 请求转化为内部的 Agent 运行任务,并保证每个请求都是独立、隔离的。

我这里的标准做法是:FastAPI 的接口定义为POST /api/v1/agent/run,请求体包含session_id、user_input、metadata三个字段。session_id必填,用来标记这是哪个用户、哪段会话的请求;metadata用来带一些业务参数,比如用户角色、租户 ID,供安全策略层判断权限。

接着在 FastAPI 的依赖注入里,我按请求粒度创建 Agent 运行实例,不搞全局单例。因为全局单例意味着所有用户共享一份运行时状态,并发一上来就是灾难。按请求创建实例的开销远没有你以为的那么大——LangGraph 的编译图是可以复用的,只是 state 是新的,真正贵重的模型调用不会因为多创建一个实例就多花钱。

4.3 LangGraph 的图结构设计:编排、工具、检查三位一体

LangGraph 应用的核心是定义一张执行图。我习惯把图设计成下面这些核心节点:

  • planner:接收用户输入,生成初步执行计划
  • execute_tool:执行具体工具调用,这是最可能出错的环节
  • inspect_result:校验工具返回结果,决定下一步是继续、重试、还是终止
  • final_answer:生成最终回复

边上就是流转条件。比如inspect_result节点判断结果合法,就走final_answer;判断结果不合法但重试次数没到上限,就回到execute_tool;判断连续重试失败,就走失败兜底节点,返回一条“我暂时无法完成这个任务”的稳妥回复,而不是让模型硬编一个答案。

这套设计的精髓在于:把 Agent 的每一步都暴露出来,你可以在任意两个节点之间插入新逻辑,比如加一个“人工审批节点”或者“权限确认节点”,完全不影响整体架构。

4.4 工具的契约化封装:参数 Schema 与结果校验器

工具是 Agent 的“手脚”,也是最容易出问题的地方。我在封装工具时强制遵循一套契约:

  • 每个工具必须声明自己的参数 JSON Schema,明确每个字段的类型、是否必填、取值范围
  • 每个工具必须返回统一的结构,至少包含success和data两个字段
  • 每个工具必须能处理自己不认识的输入,返回错误而不是抛异常

然后在调用工具之前,我会加一个参数校验层,用 JSON Schema 校验模型生成的参数。不通过就重试一次,让模型琢磨一下重新填,连续两次不过就降级到“该工具不可用”,不让错误参数打到真实系统。

结果校验这块容易被忽视。很多 Agent 工具调用之后压根不检查返回值,默认工具返回的一定是对的。实际上工具返回的数据可能本身就有问题——比如数据库查询结果是空、第三方 API 返回了错误码、文件内容解析失败。所以在inspect_result节点里,我会针对每个工具写一个轻量的校验函数,至少检查返回结构和关键字段的合法性。

4.5 并发与重试机制:别把模型调用和业务逻辑绑死在同一个线程

说到这里,终于可以正面回答“AI Agent 怎么扛并发”这个问题了。

首先,你得明确一个事实:单机同步串行地跑 Agent,靠加机器堆性能是最笨的办法,而且模型 API 的响应速度就摆在那,动不动几秒到几十秒。扛并发的核心不是“让单个 Agent 跑得更快”,而是“让系统能同时跑很多个 Agent,并且互不干扰”。

我的架构是这样:FastAPI 接收到请求后,通过消息队列或者异步任务队列把任务提交给 Worker 进程,而不是在请求线程里同步等待模型推理结束。这样 Web 层保持高吞吐、低阻塞,Agent 的整个执行过程在 Worker 中异步完成,结果通过 WebSocket、轮询或者回调推送给前端。

针对模型调用的并发控制,我用两层策略:第一层是每个 API Key 的调用限额(RPM/TPM)管理,超了就在本地排队,而不是死命往模型 API 上打;第二层是重试机制,对 429 限流错误和 5xx 服务端错误做指数退避重试,而不是失败了就让整个任务挂掉。

具体参数我是一个起跳值然后按实际压测调整的:

  • 第一次重试延迟 1 秒,第二次 2 秒,第三次 4 秒,最多重试 3 次
  • 对 429 错误,读取响应头里的Retry-After字段,按服务器要求等待
  • 对超时请求,超时时间设为 30 秒,连续超时就降级

这些参数一开始是拍脑袋设的,后来在压测环境里跑了一遍才定下来。建议你也拿自己业务的数据量去压一压,别直接抄它的数字。

5. 稳定性问题的排查与调优实录

5.1 我的“高频翻车”问题速查表

调试 Agent 的过程就像在解谜。下面这个表是我实际工作中整理出来的高频问题速查表,每次排查问题时先对着它过一遍,通常能省下一半以上的时间:

现象可能原因排查思路常见解决手段
模型回复里多出解释性文字Prompt 约束力不够/模型能力不足看完整输出,确认是否违反格式化指令改用结构化输出/函数调用,加输出校验器
工具调用参数频繁错误模型没理解工具 Schema查看日志里模型生成的参数和 schema 对比简化工具描述,给明确示例,必要时分拆工具
任务执行到一半状态丢失并发环境状态互相覆盖检查 session 隔离逻辑,全局变量污染按 session_id 隔离状态对象,禁止用全局变量
同一个问题每次答案不同缺乏确定性控制/温度太高对比模型参数和上下文组装逻辑调低温度,固定 top_p,必要时缓存相似问题答案
流程在某个工具调用上反复重试工具本身的可调用性差/参数错误看重试日志确认是哪种错误类型修复工具,补充参数校验,设置重试上限
API 返回 429 限流并发量超过模型接口上限检查队列积压和 API Key 用量本地限流排队,多 Key 负载,指数退避
Agent 磨蹭很多轮才结束规划节点没有做好任务收敛看规划输出和最终轮次是否冗余设置最大迭代次数,在提示中强调收敛
用户传入非预期内容导致整个链路异常缺少输入清洗和兜底策略查看原始输入传入流程的情况加输入过滤器,非法输入直接走固定回复

排查时我记得最重要的一点是:先定位是“模型问题”“工具问题”还是“编排问题”,再动手改。三个层面的修复方式完全不同,改错了地方不但没用,还会引入新的不稳定因素。

5.2 案例复盘:一次真实的并发问题定位

分享一个具体的排查过程,也许能帮后来人少走弯路。

那是一个内部工具型 Agent,上线第二天就有同事反馈:“我这边提交了一个问题,结果看到了别人正在处理的记录。”我第一反应是状态隔离失效了。翻了代码,发现确实如此:我在模块层面定义了一个CONVERSATION_STORE = {},用 session_id 做 key 来取上下文。看起来有隔离,但实际上多个协程并发读写这个字典时,Python 的字典操作本身不保证进程内多线程安全,虽然没有直接崩,但在高并发时数据错乱就出现了。我改成用threading.Lock包装所有读写操作之后,问题初步缓解,但这是我第一次意识到“用内存做会话存储”在并发场景下有多脆弱。

后来我把会话存储迁移到了 Redis,按 session_id 存储状态对象,并设置过期时间,才算真正解决问题。这个经历让我记住一句话:生产环境里,不要自己造轮子管理有状态数据,用现成的、经过验证的存储层,哪怕只是多引入一个 Redis 实例。

5.3 重试策略的“度”:什么该重试,什么不该重试

设计重试策略时最容易犯的错误是“什么错都重试”。实际上,你得先分清错误的性质。

  • 对暂时性错误(网络抖动、超时、限流),重试是有意义的
  • 对确定性错误(参数校验不过、权限不足、输入非法),重试毫无意义,只会浪费时间和 token

我踩过的一个坑是:某个 Agent 在调用数据库接口时,用户本来就没有权限,但系统一直自动重试了三次,用户等了好几秒只得到一个“还在处理中”的提示。这种场景的正确设计是一开始就给出明确的“权限不足”拒绝,而不是硬着头皮重试。

所以我现在设计重试逻辑时会加一个判断:只有异常类型匹配“可重试集合”的才进入重试分支,其余错误直接快速失败。可重试集合一般包括超时、连接错误、429/5xx 响应;而 4xx 类的业务错误和参数校验错误,统统不重试。

6. 关于 Agent 中台化与产品化的进一步思考

6.1 从“一个 Agent”到“一堆 Agent”:中台化的必要性

单个 Agent 的稳定性问题解决之后,紧接着来的问题是规模化的:公司里有多个业务团队,每个团队都要搞自己的 Agent,如果每个人都从零开始搭一套 Harness,地基部分全是重复劳动,标准还不统一,最后运维的复杂度会爆表。

这就是“AI Agent 中台”出现的根本原因。我理解的中台不是一套炫酷的管理界面,而是把 Harness 工程里那些公共能力抽出来做成平台服务,包括:

  • 模型网关:统一接入各家模型、统一限额、统一降级
  • 工具注册中心:统一管理工具定义、权限、灰度
  • 状态存储:统一的会话存储和状态快照服务
  • 可观测性:统一的链路追踪、日志采集、指标上报
  • 安全中心:统一的隐私过滤、敏感信息脱敏、权限校验

业务团队只需要基于中台开发自己的“流程编排”和“业务工具”,不再关心模型怎么路由、状态怎么存储、日志怎么采集。这种分层方式,本质上是把 Harness 工程从“项目里的代码”提升到了“组织级的基础设施”。

6.2 产品化中的取舍:功能广度 vs 确定性的权衡

产品化 Agent 时还会遇到一个现实矛盾:功能越广,确定性越差。一个只做订单查询的 Agent 和一个什么都能聊的 Agent,前者的稳定性大概率远好于后者,因为它的行为边界清晰、工具集小、可预测性强。

我的取舍原则是:能用规则和流程解决的问题,就不要让模型“自由发挥”;只有真正需要理解、归纳、决策的部分才交给模型。典型的做法是给 Agent 定义“能力边界描述”,当你把 Agent 的定位和官方文档写清楚之后,再在 prompt 里明确告诉它“你只负责 A、B、C 三类任务,其他问题一律回复无法处理”。这看起来减少了 Agent 的“能力”,但换来的是用户可以预期的稳定体验。

6.3 避坑指南:这几个错误一定要少犯

最后整理几条我觉得最有价值的实战避坑心得,送给正在构建 Agent 的朋友们:

  • 不要在 Agent 内部裸奔式地调用任何带副作用的外部系统,所有工具调用都要有幂等设计和失败回滚
  • 不要在 prompt 里写一些含糊的“不要”指令,比如“不要输出多余内容”,而是直接用输出格式或者结构化工具约束它
  • 不要忽略超时与中断的处理,用户可能等不及直接关掉页面,但 Agent 可能还在后台傻傻地跑
  • 不要以为模型升级就可以自动解决稳定性问题,模型越强,它能搞出的新幺蛾子可能越隐蔽
  • 不要跳过压力测试,一个 Agent 在单用户下表现得再完美,都不代表它在 50 个并发请求下还能站得住

我自己走过的弯路是:初期过度信任模型的工具调用能力,导致所有稳定性努力都押在“模型不会填错参数”上。现在我的态度刚好反过来——默认模型一定会出各种意外,把每一个环节都当成可能出错来设计,系统反而稳定多了。这种感觉,就像写后端时默认“调用方一定会传非法参数”一样,是个心态问题,但直接影响系统的每一行设计。

这个内容后续其实还有很大的扩展空间,比如基于反馈数据自动优化 Prompt、用评测集对 Agent 质量做持续回归、把 Harness 能力和现有 DevOps 体系打通。如果你正在做 Agent 开发,建议先把这篇文章里讲的状态隔离、工具契约、可观测性、重试策略这四件事落地,跑通了再谈花活儿。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询