做 AI Agent 项目做到第五个版本的时候我终于承认一件事模型本身不是最大的瓶颈围绕模型的工程约束才是。标题里那句“构建稳定的 AI Agent”听起来像是网络上随手一搜就有的泛泛之谈但真把 Agent 丢到生产环境里跑上一周你才会明白什么叫“稳定是稀缺品”。今天这篇东西我不打算讲什么大而全的架构蓝图只想围绕 Harness 工程给 Agent 套缰绳的工程实践这个核心聊聊 Agent 为什么不稳定、怎么让它稳定、以及我在实际项目中踩过的坑和沉淀下来的做法。如果你正在用 Claude Code、LangGraph、FastAPI 这类工具搭自己的 Agent或者被“Agent 能跑但不敢上生产”折磨过这篇应该能给你一些能直接落地的参考。1. 为什么 Agent 总是不稳定从一次“翻车”说起先说一件让我彻底改变思路的事。早期我做过一个内部用的数据查询 Agent模型用的是当时最强的闭源模型工具链也齐全——能查库、能调接口、能生成报表。Demo 阶段一切完美但一放到真实业务环境问题就来了用户随口一句“上个月华东区的退货率怎么样”它能正确把 SQL 写出来却因为表名权限配错反复重试三次后开始胡编一个数值另一个场景里它拿到一个模棱两可的需求不主动确认直接按最坏的猜想去执行把一批测试数据给覆盖了。那段时间我几乎每天都在“救火”后来复盘时发现模型本身的推理能力没有任何问题问题出在“没有缰绳”。它不知道自己的权限边界是什么不知道什么情况下该停下来问人不知道重试几次后必须走回退路径。这个认知直接把我推向了 Harness 工程——也就是给 Agent 套上一整套行为约束、工具权限、状态管理和回退机制。1.1 所谓 Harness 工程给 Agent 套上“缰绳”Harness 这个词英文原意是马具、缰绳。用在 AI Agent 领域指的是围绕 Agent 构建的一层“约束与支撑系统”既给它动力也限定它的活动范围。这个说法在 Claude Code 的实践社区里传播得很广大家讨论的 harness 工程之道核心就是通过 system prompt 管理、工具白名单、权限控制、结果校验、回退策略等手段让模型在可控边界内发挥能力。打个生活化的比方一个刚拿到驾照的新手司机车是好车动力也足但你不能直接让他上高速。你得先给车装上辅助刹车、车道偏离预警再限定他只能在固定路线上开副驾还得坐个教练。Harness 就是这套辅助系统和教练规则的总和。模型是司机Harness 是车上的安全机制和交规。这里有个容易混淆的概念很多开发者把“写好 system prompt”等同于“做好 Harness”。这是完全不够的。System prompt 只是约束的一部分而且是最容易被模型“选择性忽略”的那部分。真正的 Harness 必须包含硬约束——工具调用权限由代码拦截、重试次数由代码控制、输出格式由代码校验这些不能指望模型“自觉”。1.2 稳定不是玄学先把三个问题说清楚在做 Harness 之前得先把“不稳定”拆开看。我在项目里把 Agent 的不稳定归成三类每一类都有不同的解法切入点。第一类是决策不稳定。同一个问题问两次Agent 给的方案不一样甚至第二次给出的方案是错误的。这类问题根源在于模型采样有随机性以及上下文里的无关信息干扰了推理。解法方向是降低温度参数、精简上下文、用结构化 prompt 模板。第二类是执行不稳定。Agent 计划做得挺好但执行时工具调用失败、参数传错、接口超时然后它情绪化地“硬编”一个结果出来。这类问题根源在于缺少对工具返回值的强制校验和失败重试策略。解法方向是给每个工具调用设计明确成功/失败判定标准。第三类是资源不稳定。并发一上来token 消耗失控、队列堵塞、超时率飙升。这类是纯工程问题模型能力再强也救不了必须在架构层面做排队、限流、超时管理和并发隔离。把这三类问题一一对应到 Harness 的几个机制上整个工程思路就清晰了。接下来我详细拆解这些机制。2. Harness 工程的四个核心机制拆解很多人第一次听到“Harness 工程”会觉得这是个新鲜概念但拆开看它其实就是一套组合拳——把传统后端开发里成熟的手段权限控制、重试机制、状态机、可观测性应用到 Agent 场景里。下面这四个机制是我在每个项目里都会落地的核心缺一个稳定性就会明显打折。2.1 角色与上下文约束让 Agent 知道“我是谁、能干什么”第一层约束是角色系统。这里说的角色不只是 system prompt 里写一句“你是一个乐于助人的助手”它要回答的是三个具体问题你是谁、你能调用什么工具、你不能做什么。我在实际配置里会把角色定义拆成五段式的模板身份定位一句话说明 Agent 服务对象和领域边界例如“你是电商数据分析助手只处理订单与商品相关的查询”工具清单明确列出可用的工具名称而不是让模型猜测例如“可用工具search_orders、get_product_info、generate_report”硬性禁止明确列举绝对不能做的事例如“禁止执行删除操作”“禁止越过查询权限直接访问原始表”不确定响应策略遇到模糊问题时必须向用户确认例如“当查询条件缺失时列出你理解的参数并要求用户确认”输出格式要求定义结构化输出模板例如“必须返回 JSON包含 data 和 confidence 两个字段”。这段 prompt 看起来简单但效果非常显著。它本质上是在模型推理之前先圈定了一个“求解空间”。实验数据上加了硬性禁止和不确定响应策略之后Agent 在模糊任务上的误操作率能下降一半以上。这里有一个值得单独强调的细节角色约束不要试图用“禁止”去覆盖模型的所有潜在错误行为因为列不完。正确的做法是抓大放小——只禁止那些会给系统带来不可逆影响的行为删除、写库、打钱其余行为交给工具权限层去拦截。Prompt 是软约束工具权限是硬约束两者的配合才是完整的 Harness。2.2 工具权限与白名单把每把刀都收进刀鞘如果说角色约束是“教育”那工具权限就是“制度”。我给 Agent 的每个工具都设置了访问级别核心是“按最小权限原则”配置。不是 Agent 能调什么就给它什么而是它需要什么才给它什么。实际项目里我维护了一张工具权限矩阵表大致长这样工具名称功能说明权限级别允许调用条件search_orders查询订单只读任意会话get_customer_info查询客户信息只读(脱敏)需用户明确授权update_order_status更新订单状态写操作需二次确认delete_record删除数据禁止调用永不开放这张表落到代码里就是在工具注册时增加一个 middleware 层每次模型发起工具调用请求先由代码检查该工具的权限级别、调用参数、是否满足条件不满足的直接拒绝并返回提示。权限检查放在模型调用层之外这样即使模型“想”越权它也没有能力越权。这个机制解决了一个很隐蔽的问题模型经常会“顺手”做一个超出用户预期的操作。比如用户问“这个订单怎么没发货”模型可能为了展示能力直接调用了更新状态的工具把订单给改了。有了工具权限层这种操作会被代码拦下来强制转回“仅查询”路径。另外工具调用的输入也需要做 schema 校验。模型生成的参数偶尔会是错的——字段名称写错、枚举值不在范围内、时间格式不符合预期。用 JSON Schema 对参数做一层强校验能够把大部分参数错误拦截在真正调用之前。校验失败的信息要回传模型让它“自我修正”一次再失败就走回退。2.3 护栏与回退机制错误不是失败是必经路径我在给团队做分享时反复强调一句话不要试图让 Agent 不犯错要预设它会犯错并且为错误设计好逃生通道。护栏机制的核心是一个三层回退金字塔越往下越保守。第一层是自动修正工具调用失败时把错误信息返回给模型让它在限定次数内重试。这一层适合参数小错误、临时超时这类问题。第二层是降级方案自动修正失败后切换到一个更简单的工具或默认策略。比如查询接口超时那就先查本地缓存副本缓存也没有就返回一个“数据暂不可用”的明确结果而不是硬编一个数。第三层是人工接管所有自动路径都失败时生成一个可读的错误报告标记该会话为“需要人工介入”推给值班人员处理。这三层回退必须显式写在代码里而不是靠 prompt 里的“如果失败请重试”。我见过太多项目把回退逻辑写在自然语言里结果 Agent 在失败后仍然自我发挥产生更离谱的结果。一个具体的经验重试次数不要设置为固定值最好与错误类型关联。网络超时类错误可以重试 2-3 次参数校验类错误重试 1 次因为参数错说明模型理解有问题再试大概率还是错鉴权类错误直接放弃并升级人工。固定重试次数往往是制造“重试风暴”的元凶。2.4 任务分解与状态管理把大目标切成可回滚的小步骤Agent 的另一个不稳定源是“一次性完成一个大任务”。比如让 Agent“分析近三个月所有品类的销售趋势并生成 PPT 报告”这么大的目标模型在执行过程中很容易丢步骤、卡中间、或者结果和用户预期南辕北辙。Harness 工程在这里的解法是任务分步化与状态持久化。也就是把一个大的 Agent 任务拆成多个可以独立验证的小步骤每一步都记录状态进行中/成功/失败失败时可以从最近的成功状态继续而不是整个任务推倒重来。技术实现上用 LangGraph 这类图状态框架会非常方便。LangGraph 天然支持把 Agent 的 workflow 建成一张状态图每个节点是一个处理步骤节点之间有明确的转换条件每一步的执行结果都写入持久化存储比如 Redis 或数据库。这样即使进程崩溃、网络中断恢复后也能从 checkpoint 继续而不是让用户重新问一遍。我在实际项目里常用的状态结构大概长这样{ task_id: task_20250212_001, status: in_progress, current_step: data_fetch, steps: [ {name: query_parse, status: done, output: {...}}, {name: data_fetch, status: in_progress, output: null} ], checkpoints: [query_parse], created_at: 2025-02-12T10:00:00Z, updated_at: 2025-02-12T10:01:30Z }每个 checkpoint 都保存了该步骤的输入输出摘要这样模型在下一步推理时可以只加载相关的历史上下文而不是把整个对话历史全塞进去。既省 token也减少无关信息干扰。3. 实操一个可复现的稳定 Agent 搭建过程讲完机制落到实操。我以目前最顺手的一套技术组合为例——FastAPI LangGraph 一个支持 function calling 的模型——带大家完整走一遍搭建过程。这套组合的好处是FastAPI 负责外部接口和并发控制LangGraph 负责 Agent 内部的状态流转与任务图模型只负责“决策”而不是“管理流程”职责分离得很干净。3.1 技术选型与架构FastAPI LangGraph 的取舍选型阶段我比较过几套方案。第一套是纯 LangChain 的 AgentExecutor简单但可控性差内置的 Agent 循环像一个黑盒你很难在中间步骤插入校验逻辑。第二套是自研状态机加模型调用可控性最强但要写的代码太多不适合快速迭代。第三套就是 LangGraph它把状态图、条件跳转、checkpoint 都封装好了同时保留了足够的自定义空间是在开发效率和可控性之间最平衡的选择。FastAPI 作为接入层则是顺理成章的选择。它天然支持异步配合 asyncio 队列可以做请求排队配合 Redis 可以做分布式限流而且 OpenAPI 文档能直接暴露给前端团队联调。整个架构的分层是这样的层级职责技术选型接入层HTTP接口、鉴权、限流、排队FastAPI Redis编排层Agent状态图、节点跳转、checkpointLangGraph决策层模型调用、工具选择支持 function calling 的 LLM工具层实际业务操作查库、调接口自研工具集 权限middleware数据层状态存储、对话历史、缓存Redis PostgreSQL这个分层的核心思想是不要让模型直接接触工具层模型只能通过编排层的中介去调用工具。编排层负责校验、记录、重试、回退所有“非智能”的确定性逻辑都从模型手里接管出来。3.2 把 Harness 落进代码角色、工具、护栏的配置示例下面我贴一段基于 LangGraph 的简化代码展示 Harness 最关键的工具权限拦截和重试回退是怎么写的。这段代码我做了简化但核心逻辑是完整可用的。from fastapi import FastAPI, HTTPException from langgraph.graph import StateGraph, END from pydantic import BaseModel, ValidationError from typing import TypedDict, Optional import asyncio import json # 1. Agent 状态定义 class AgentState(TypedDict): user_query: str current_tool: Optional[str] tool_result: Optional[str] retry_count: int final_answer: Optional[str] need_human: bool # 2. 工具权限矩阵硬约束 TOOL_PERMISSIONS { search_orders: {level: read, allowed: True}, get_customer_info: {level: read_masked, allowed: True, require_authorization: True}, update_order_status: {level: write, allowed: False, require_confirmation: True}, delete_record: {level: destroy, allowed: False}, } # 3. 工具调用统一入口中间层拦截 async def call_tool(tool_name: str, params: dict, user_context: dict) - dict: perm TOOL_PERMISSIONS.get(tool_name) # 硬约束检查 if perm is None: return {ok: False, error: tool_not_found, message: f工具 {tool_name} 不存在} if not perm[allowed]: return {ok: False, error: permission_denied, message: f工具 {tool_name} 未授权调用} if perm.get(require_authorization) and not user_context.get(authorized_tools, {}).get(tool_name): return {ok: False, error: auth_required, message: f工具 {tool_name} 需要用户明确授权} # 参数校验这里以搜索订单为例 if tool_name search_orders: try: validated SearchOrdersParams(**params) except ValidationError as e: return {ok: False, error: invalid_params, message: str(e)} # 实际业务调用... result await do_search_orders(validated) return {ok: True, data: result} # ... 其他工具分支 # 4. LangGraph 节点模型决策 回退逻辑 async def model_decision_node(state: AgentState) - AgentState: # 在真实项目里这里会调用 LLM 的 function calling # 这里简化为一个模拟决策 tool_name, params await llm_dispatch(state[user_query]) if tool_name is None: # 模型认为不需要工具直接回答 state[final_answer] 已直接回答用户问题 return state result await call_tool(tool_name, params, user_contextstate.get(user_context, {})) if result.get(ok): state[tool_result] json.dumps(result[data], ensure_asciiFalse) state[retry_count] 0 return state # 回退逻辑按错误类型决定是否重试 if state[retry_count] max_retry_for_error(result[error]): state[retry_count] 1 # 返回给模型进行修正这是重试 state[tool_result] f工具调用失败: {result[message]}请修正后重新调用 return state else: # 达到重试上限降级或转人工 state[need_human] True state[final_answer] 工具调用多次失败已转人工处理请稍后查看结果 return state # 5. 图编排 graph StateGraph(AgentState) graph.add_node(decision, model_decision_node) graph.add_node(respond, lambda state: state) graph.set_entry_point(decision) graph.add_conditional_edges( decision, lambda state: respond if state.get(final_answer) else decision, ) graph.add_edge(respond, END) agent_app graph.compile()这段代码有四个关键点值得细看。第一权限检查在call_tool这个统一入口里完成而不是依赖模型自觉这是硬约束的落地方式。第二重试次数跟错误类型挂钩max_retry_for_error是一个函数根据错误码返回不同上限。第三失败信息会回传给模型给模型一次修正的机会但不能无限修正。第四need_human是一个“求救”信号一旦置位整个流程就转入人工处理路径而不是让 Agent 继续低质量地硬撑。3.3 并发与资源控制让 Agent 扛得住真实流量“AI Agent 怎么扛并发”是近期被问得最多的问题之一。很多 Agent 在单用户测试时表现很好一上并发就崩原因往往是没做资源隔离和队列控制。LangGraph 本身是单会话状态机并发能力取决于你把它跑在什么样的执行环境里。我采用的方案是“FastAPI 异步 工作队列 并发隔离”。每个用户的 Agent 会话是一个独立任务放进队列由一个 Worker 池消费。关键配置有三个第一个是并发上限。通过 Web 框架的信号量asyncio.Semaphore控制同时执行的 Agent 任务数量。这个值需要根据你的模型 API 限流条件做压力测试来确定一般从 5 开始压逐步加大到 20、50观察 P95 延迟和错误率。需要注意的是并发不是越大越好盲目增加并发只会让模型 API 触发限流反而把整体吞吐拖垮。第二个是非阻塞架构。Agent 执行过程中要调用模型 API、要查询数据库这些全是 IO 操作。在 FastAPI 里必须用 async/await 把它们承接住否则一个 Agent 任务在等 API 响应时整个进程的线程就被占用了后面的任务排长队。第三个是 Redis 分布式锁。同一个用户同时开了两个会话或者同一个任务被重放了用 Redis 加一把简单的分布式锁保证同一时刻同一个任务只被执行一次避免重复写数据、重复扣费这类低级但严重的故障。这里放一个用 Semaphore 控制并发的简化例子from asyncio import Semaphore from contextlib import asynccontextmanager # 全局信号量限制同时执行的 Agent 任务数 AGENT_SEMAPHORE Semaphore(20) asynccontextmanager async def agent_task_guard(): async with AGENT_SEMAPHORE: yield app.post(/agent/run) async def run_agent(request: AgentRequest): async with agent_task_guard(): # 在信号量保护下运行 Agent 任务 result await run_agent_workflow(request) return result信号量的限制是整个进程级别的如果要做到多实例共享需要换成 Redis 的并发计数器。但对于大多数中小型项目进程内信号量已经足够。4. 高频问题与排查记录写这套系统的过程中我积累了厚厚一沓问题排查笔记。下面挑几个经典问题分享每个都是真实踩过的坑希望能帮你省掉几天的排查时间。4.1 Agent 答非所问或幻觉这是最让人头疼的问题之一。排查时要先分清是模型推理问题还是 Harness 约束问题。我的排查顺序是先看有没有工具被调用。如果 Agent 能正确选择工具并获取到真实数据但最终答案仍然出现编造内容那问题多半在“总结环节”——模型拿到了数据但没忠实引用。解决方式是育成“引用优先”的输出模板强制要求最终答案里的事实性内容必须标注来源工具和记录 ID。例如“根据订单表来源search_orders记录 ID 8842本月退货率为 3.2%”。如果模型引用的记录 ID 与工具返回不一致说明它在编造代码层需要检测到这种不一致并驳回重试。另外一个排查思路是检查你的 system prompt 是否塞了太多无关内容。上下文越长模型越容易“迷失”在无关信息里导致它忽略了关键任务指令。我测试过把一个 2000 字的 system prompt 压缩到 400 字幻觉率明显下降。Prompt 不是越长越好信息密度才是关键。4.2 工具调用失败与循环重试我在项目里遇到过最诡异的问题Agent 在一个失败的工具调用上反复重试而且每次重试参数都略有不同看起来像是在“碰运气”实则在疯狂消耗 token。这种情况的根因通常是错误信息回传给模型时不够明确模型以为自己参数传错了于是不断微调参数但真正的问题其实是后端服务宕机。解决办法是在调用失败时把错误类型明确地标记为“不重试类错误”。我采用了一个简单可行的方法错误消息带上错误码前缀。例如[RETRYABLE_TIMEOUT]网络超时模型可以重试[FATAL_INTERNAL_ERROR]服务内部错误模型不要重试[VALIDATION_FAILED]参数校验失败模型修正参数后最多再试一次。模型虽然不能 100% 理解这些标记但配合代码层的重试计数器双保险之下“重试风暴”的问题基本能被根治。4.3 并发场景下的令牌与队列问题AI Agent 项目里的 token 消耗是实打实的钱并发一上去控制不好账单就看天吃饭了。我在并发场景下踩过最大的坑是每个会话的历史消息不断累积token 用量呈线性增长最后导致单次请求超过模型上下文窗口直接报错。现在的做法是上下文裁剪加摘要压缩。对话超过一定轮数后通常是 10-15 轮把早期对话交给一个轻量模型生成摘要只保留摘要和最近几轮完整消息。这个策略能省 40% 以上的 token同时不损伤对话连贯性。另一个经验是给每个会话设定 token 预算。用 Redis 记录每个用户每小时的 token 消耗超过预算直接拒绝新请求并提示“额度已用完”。这虽然会限制用户但比起月底收到巨额账单限制用户的体验问题显然更容易接受。4.4 调试技巧从日志到追踪Agent 系统调试的难点在于“决策不可见”。你不知道模型为什么选这个工具、为什么拒绝执行、为什么走了回退分支。传统的 print 日志在单会话调试时还能用但并发场景下根本凑不齐一条完整链路。我强烈建议在 Agent 系统里接入全链路追踪最简单的做法是给每次会话生成一个 trace_id并在每个节点模型调用、工具调用、权限检查、回退分支都输出结构化日志{ trace_id: trace_7f3a9d2e, node: model_decision, action: tool_selected, tool_name: search_orders, params: {date_from: 2025-01-01}, latency_ms: 842, token_cost: 1200, timestamp: 2025-02-12T10:00:01Z }有了 trace_id用户报问题时只要把 ID 发过来我就能把整条执行链路拉出来看。排查效率至少翻倍。如果你还没有做这个建议列入下个迭代周期的第一优先级。5. 工程落地的几条个人经验最后结合我做过的几个项目聊几条工程落地层面的经验。这些不是教科书里的方法论是花了不少学费换来的。5.1 从小场景切入别想一口吃成胖子最开始我做 Agent 时总想做一个“全能的业务助手”什么都能问、什么都能做结果就是什么场景都不稳定。后来改成只做“订单查询助手”工具只有三个权限边界非常清晰一周就稳定上线了。稳定之后再逐步扩展商品查询、报表生成等能力。你每加一个工具系统的“不稳定面”就会大一圈所以宁可小步慢跑也不要一次给 Agent 太多能力。这也是 Harness 工程的核心精神给 Agent 的能力做“减法”而不是“加法”。能力少一个要约束的就少一圈稳定性自然随之提升。如果你的 Agent 经常在多个工具之间犹豫不决那大概率是工具数量超出了它的决策带宽。5.2 评测比调 Prompt 更重要我见过团队花两周时间调 prompt却没有任何一个评测数据集来度量改动好坏。这完全是本末倒置。Prompt 调整是“感觉良好”评测才是“数据说话”。我现在的做法是为每个 Agent 维护一个测试集里面固定 30-50 条覆盖正常、边界、异常场景的测试用例每次改动 Harness 配置或 prompt 后全量跑一遍用通过率判断改动是否有效。借助 LangSmith 这类平台采集真实用户交互来人工打标就能追出实际效果指标。这套流程跑起来后Agent 的迭代才真正进入工程化轨道。5.3 Harness 不是一次性工作是持续演进的约束层我第一次做完 Harness 设置时觉得终于搞定了后来发现模型升级、业务变化、新工具的加入都会让旧约束失效。比如某次模型从 V1 升级到 V2原本能被约束住的某些行为突然又冒出来了因为新模型对 prompt 的理解方式变了。所以现在我把 Harness 当做一个独立于 Agent 业务流程的“软件层”来维护。每次模型版本升级都会用评测集回归一遍所有约束是否仍然有效每个新工具的加入都要先过一遍权限矩阵评审再进测试集。这一层不是写一次就完事它是一套“活”的工程系统。就聊到这里。如果你正在做的事情跟这个类似希望这些思路能帮你少踩几个坑。尤其是那个观点——“模型负责聪明代码负责稳定”建议你从下个项目开始试试。把确定性的逻辑尽量从 prompt 里挪到代码里你会明显感受到 Agent 的可靠性上升一个台阶。