
这两年做Agent项目我发现自己最常被问的一个问题并不是“哪个模型更强”而是“Agent到底应该怎么组织才能从demo变成真正能扛事的东西”。说实话我早期在这一点上栽过不少跟头。提示词散落在各个handler里工具调用这块写一套、那块又写一套执行到一半状态丢了根本不知道从哪儿续上——一个看起来很有潜力的智能应用上线之后就是个不断吐异常的黑盒子。后来我们痛定思痛把整套系统拆成了三层Harness、Loop、Graph。这个拆分听起来抽象但它直接解决了我上面说的所有问题。Harness管的是模型怎么触碰世界Loop管的是模型怎么完成一次决策闭环Graph管的则是多个闭环怎么组织成一条可追踪、可恢复的生产流程。这篇文章把这三层架构掰开揉碎讲清楚包括每层到底干什么、层与层之间怎么协作、以及我在生产环境里踩过的各种坑。不管你是刚接触Agent开发的新手还是已经在线上被问题折腾得焦头烂额的工程师这篇内容都值得你从头到尾过一遍。1. 为什么说Agent工程需要一套三层架构1.1 把“Agent”拆成能落地的部件先统一一下认知。很多时候我们说“在做Agent”但每个人脑子里的画面完全不同。有人想的是一段脚本里循环调用大模型API有人想的是给模型套一堆工具函数还有人想的是跑一个带状态机的工作流引擎。这三种理解不冲突但混在一个项目里就会变成灾难。我比较推荐的三层拆法是这样的Harness层负责模型和外部世界的适配。工具注册、技能装载、权限控制、上下文注入都在这一层完成。它不负责“思考”只负责“行动能不能被安全地执行”。Loop层负责单次任务的认知闭环。模型观察现状、决定动作、执行工具、再看结果这个循环就是Loop。它把“一次思考-行动-观察”的过程做成可控的流程而不是裸调一次接口。Graph层负责多步骤、多分支、跨时间恢复的全局编排。Graph把多个Loop、多个Harness调用、甚至人工审批节点组织成一张有向图并负责状态的持久化和恢复。你可以这么理解Harness是“机器手臂”Loop是“一个完整的动作循环”Graph是“整个生产线的调度系统”。没有Harness模型只是键盘侠没有Loop模型做一步就停没有Graph整个系统就是一团乱麻出了问题连从哪一步开始排查都不知道。1.2 三层各管什么边界在哪里边界清楚是这套架构最大的价值。我见过很多项目工具调用逻辑和业务逻辑混在一起循环逻辑和流程编排逻辑混在一起最后的结果就是改一个功能要动三个模块。我习惯用一张职责表来约束自己和团队层级核心问题主要职责典型产出Harness模型如何安全地调用外部能力工具注册、参数校验、权限控制、技能装载工具函数、Skill包、权限策略Loop模型如何完成一次闭环决策上下文管理、终止判断、步数控制、异常兜底循环执行器、终止策略、消息记录Graph多个决策如何组织成生产流程节点调度、分支选择、状态持久化、重试恢复状态图、拓扑定义、快照存储为什么一定要把Harness单独拎出来因为现实里的工具接入远比想象中复杂。一个查询天气的接口看起来只是发个HTTP请求但生产环境里你要处理认证、限流、结果归一化、超时重试、敏感信息过滤。这些东西如果直接写进Loop每次循环里都带着一堆业务判断代码很快就没法看了。而Graph独立成层的意义在于它让开发者可以用“图”的视角审视整个系统。并行节点、条件分支、人工审批这些在代码里硬写会极度混乱但抽象成Graph之后就变成了节点和边的配置。出了问题你甚至可以回放状态快照精确复现每一步发生了什么。1.3 一次完整请求的三层协作过程用一个真实场景串一遍。假设业务是一个企业IT工单助手用户问“我想重置域密码。”请求进来之后Graph层先接手。它看到这是一个“密码重置”意图于是规划出几个节点校验用户身份、检查工单系统状态、执行密码重置、通知用户。进入第一个Loop节点后模型开始工作。它发现需要调用身份校验接口于是向Harness询问有哪些工具可用。Harness返回一个名为verify_sso_token的工具模型生成调用参数Harness完成参数校验和权限检查然后真的去调SSO系统。返回结果后Loop把观察结果追加到上下文模型继续判断下一步。如果一切顺利整个流程在Graph的调度下推进到“通知用户”节点循环结束状态快照写入存储。如果中途某个节点失败了Graph可以根据预设的重试策略拉起重跑或者转入人工节点。你看这三层各司其职Harness不决定流程Loop不自己选工具Graph不关心API调用的细节。每层出问题都能在对应层定位和修复。2. Harness层连接模型与真实世界的工具总线2.1 Harness到底是什么不是什么社区里关于Harness的讨论挺多的尤其是DeepSeek、Claude Code这类工具兴起之后“harness”这个词频繁出现在插件和技能装载的语境里。简单说Harness就是一套把“模型发出的行动意图”翻译成“真实系统调用”的机制。它不是Agent本身。Agent是会推理、会做决策的主体Harness更像是给Agent穿上的“外骨骼”决定了它能碰什么、不能碰什么、用什么姿势碰。以DeepSeek harness这类开源项目为例它的核心功能就是给你用的模型挂载工具集和Skill包——模型只负责生成调用参数真正去执行搜索、读文件、调API的是Harness。Harness也不是简单的API网关。网关做的事是把请求转发给后端服务但Harness要做的事更多它要理解模型输出的结构化调用参数要做参数校验要把不同工具返回的不同格式统一成模型能理解的格式还要处理权限、限流、审计这些生产级问题。2.2 工具接入的三步法声明、校验、归一我总结的工具接入流程三步基本够用也是我每次给团队培训都会强调的。第一步声明。每个工具必须有一个机器可读的声明文件。用Python写我一般用Pydantic模型做参数Schema声明同时挂上描述信息。这一步做不好后面所有环节都会出问题因为模型靠你的声明来生成调用。from pydantic import BaseModel, Field class QueryWeatherParams(BaseModel): city: str Field(..., description城市名比如北京、上海) unit: str Field(celsius, description温度单位celsius或fahrenheit) harness.register( namequery_weather, description查询指定城市的实时天气支持国内主要城市, params_schemaQueryWeatherParams, timeout10, ) def query_weather(city: str, unit: str celsius) - dict: # 真实实现 result requests.get(fhttps://weather.example.com/api?city{city}) return result.json()第二步校验。模型生成的参数经常是“看起来合理实际用不了”。city可能拼错unit可能填成“fff”。Harness必须在调用真实API之前做严格校验校验不过就返回一个清晰的结构化错误而不是把脏参数带到上游系统。这一步能挡住大量线上事故。第三步归一。工具返回的数据格式五花八门有的是JSON有的是纯文本有的是报错。Loop里的模型不关心你接的是哪个厂商的接口它只关心Harness给它返回了一个统一的格式。我用的归一化结构是def ok(data): return {ok: True, data: data} def fail(error_code, message): return {ok: False, error_code: error_code, message: message}统一成{ok: bool, data: ..., error: ...}之后Loop处理观察结果的逻辑就会特别简单不需要写一堆分支去猜“这个工具到底返回了什么”。2.3 Skills技能的挂载与内网部署Harness层除了注册工具函数还有一个重要职责是装载Skills。Skill和单个工具的区别在于Skill往往是一个完整的“能力包”包含系统提示词、若干脚本、资源文件、调用入口。比如一个“网页搜索技能”里面可能有搜索脚本、结果解析器、摘要提示词。以DeepSeek harness的工作流插件为例一个Skill包通常长这样skills/ web-research/ skill.yaml search.py summarize.py prompts/ system.mdskill.yaml里声明技能名称、描述、入口脚本、参数规范。Harness启动时扫描skills目录并注册所有合法Skill。这里我强烈建议把“技能描述”写得极其详细因为模型选择技能时靠的就是这段描述。描述写得太泛模型该用的时候不会用写得太具体又可能错失相近场景。内网部署的时候最稳妥的做法是把Skill包打成带版本号和哈希校验的离线包部署脚本先验哈希再解压到指定目录。不要直接从公网拉取依赖也不要用“最新版”这种漂移式的版本声明。生产环境的包管理讲究“锁定一切”。装完之后写一个简短的冒烟测试确认每个Skill都能被Harness正常加载。2.4 踩坑实录harness failed to load plugins这个报错我见过太多次了十次里有八次是下面三个原因之一原因一插件目录权限。Harness进程是普通用户跑的但Skills目录是管理员手动创建的目录权限不对插件扫描器读不了文件就直接跳过或报错。解法很简单目录属主改成跑服务的那个账号权限至少755。原因二manifest格式问题。skill.yaml里字段名拼写错误、缩进不对、编码不是UTF-8Light加载器解析失败。这个很隐蔽因为YAML解析器报错信息有时候非常误导。我的排查习惯是先用Python单独加载一次YAML文件看能不能解析成功再把字段名和代码里读取的字段名逐一比对。原因三依赖缺失或版本冲突。Skill脚本里import了一个库但Harness的运行环境没装或者装了不兼容的版本。这种问题在导入阶段不会立刻炸出来而是在真正调用Skill时才报“module not found”。我的做法是在Harness启动阶段做一次“预载检查”把每个Skill的入口脚本都import一遍发现问题启动即报而不是等线上请求来了才炸。遇到failed to load plugins第一步不是看代码而是看Harness日志。大多数Harness框架会打印出每个插件加载的详细状态问题出在哪个插件、哪一行日志里通常有线索。拿到线索再逐项排查比无头苍蝇式改配置高效得多。3. Loop层把一次决策做成可控的认知循环3.1 从一次调用到循环如果你对Agent的理解还停留在“调一次大模型接口拿回一个结果”那你就还没进入Agent的世界。真正的Agent应用核心是“循环”模型生成行动、执行行动、观察结果、再生成行动……直到问题解决。这个循环的学术名字叫ReActReasoning Acting。你可以把它理解成一个人解决问题的过程先想清楚现状再做一步操作然后看操作结果根据新情况想下一步。Loop层的职责就是把这个过程工程化让它可控、可靠、可观测。一个最简Loop的骨架长这样def run_agent_loop(messages, system_prompt, harness, max_steps12): messages [{role: system, content: system_prompt}] messages for step in range(max_steps): response llm.chat(messages, toolsharness.tool_schemas()) messages.append({role: assistant, content: response.content}) tool_calls response.tool_calls if not tool_calls: return response.content for call in tool_calls: observation harness.execute(call.name, call.arguments) messages.append({ role: tool, tool_call_id: call.id, content: observation, }) return step_limit_reached3.2 终止条件与步数上限别让循环变成死循环Loop设计里最容易忽略的就是“什么时候停”。模型自己是没有停这个意识的你给它一个模糊任务它可能真的会循环二十步还不收敛Token烧到爆。我常用的终止条件有这么几种没有新的工具调用模型直接给出了自然语言回复说明它认为任务完成了。达到最大步数上限我默认设置12步大部分任务在3到8步能完成。超过12步的要么是问题太复杂要么是模型在兜圈子。检测到空转连续两次工具调用相同、参数相同且没有新信息进来直接终止。这个技巧能救不少Token。还有一点容易被忽略步数上限本身就是一个“护栏”。线上环境不要把上限调到太大我见过有人图省事设到50步结果一个失控的Agent连续调用外部API差点把下游系统打垮。宁可让复杂任务失败后走人工流程也不要让一个循环在生产环境里狂奔。3.3 上下文管理循环越长Token越失控循环的每一步都会追加模型输出、工具观察结果消息列表越来越长。到了第8步可能已经积累了上万Token其中大部分是中间步骤的噪音。这既烧钱又会干扰模型对最新状态的判断。我的做法是三层过滤只保留必要中间结果工具观察结果在模型生成的下一步行动后其实已经没什么用了。除了关键数据比如检索到的一小段原文可以在追加后做截断。摘要压缩当上下文超过阈值把较早轮次的历史用大模型生成一段摘要替换掉原消息。这被大多数人叫“Memory压缩”本质是给Loop减负。固定System Prompt位置System Prompt永远保持在最前面不要被中间的循环消息挤下去否则模型对自身能力的认知会漂移。3.4 并发场景AI Agent到底怎么扛并发“AI Agent怎么扛并发”这个话题从我接触生产环境开始就一直在被问。很多人以为Agent扛并发就是堆机器其实核心矛盾不在计算资源而在“状态共享”和“上下文污染”。我先说一个我自己踩过的坑。早期我用一个Python列表存所有会话的上下文跑单请求没问题一上并发就乱套用户A的消息串到了用户B的会话里回答牛头不对马嘴。这就是典型的状态污染。解法很简单把状态从上到下隔离请求级隔离每个HTTP请求创建独立的Loop实例和上下文消息列表绝不共享可变数据结构。状态外置循环的中间状态不要放在进程内存里而是放到Redis这类外部存储按thread_id做键。这样即使进程崩溃下一个实例也能从Redis恢复。沙盒池化如果Agent需要执行代码、操作浏览器这类重环境预先启动一批沙盒放进池子请求来了从池里取用完归还而不是每个请求临时创建。实测这样能让p95延迟降低40%以上。限流保护下游模型API和应用API都要做令牌桶限流。不然突发流量会把所有依赖的服务打挂Agent再聪明也没用。我们线上做过一轮压测单实例、12并发、每个请求3到6步循环在状态外置和实例隔离的条件下成功率99.7%p95延迟基本只受模型API延迟影响互相之间完全没有干扰。这说明只要状态隔离做到位Agent服务和其他常规后端服务在并发处理上并没有本质差异。3.5 常见错误agent execution terminated due to error这个报错信息是很多人在日志里看到过的大魔王。字面意思是“Agent执行因错误终止”但真正的原因千奇百怪。我整理过日志里的一些高频case工具调用抛异常没被接住Harness执行一个工具时抛了异常Loop没有兜底整个循环直接终止。解法是在每次工具调用外面套一层try/except把异常转成结构化错误观察结果返回给模型让模型有机会调整策略。状态配置缺失Graph层传递的某个状态字段在Loop里根本不存在一访问就报错。解法是在Loop入口做状态Schema校验。下游API超时模型API本身能跑但调用的外部接口慢导致循环整体超时。解法是给每个工具单独设超时HTTP客户端用连接池和超时熔断。消息格式不合规某些框架要求消息里必须有tool_call_id结果你漏了整个会话被终止。解法是每次追加消息前做结构校验。说白了这类问题的根源大多是“异常处理不彻底”。把循环里的每一步都包上异常边界错误信息结构化整个Loop的鲁棒性会提升一个档次。4. Graph层从单任务循环走向全局状态编排4.1 为什么单Loop解决不了生产问题Loop解决的是“单个任务怎么做闭环”。但生产里的业务很少只有一个任务。一个客服Agent可能要经过理解意图、查知识库、调工单系统、生成回复、人工复核、发送结果中间还有条件分支和并行操作。你要是把这些全塞进一个Loop里模型要在一个循环里处理所有分支逻辑效果会非常差。更麻烦的是可维护性半年之后你会发现加了十几个if分支的Loop谁都不敢改。而Graph层的出现就是把“流程控制”从“模型推理”里剥离出来。流程的每一步去哪里、什么条件下走哪条边是工程上定义的拓扑而不是模型临场发挥的结果。4.2 图的三个核心概念节点、边、状态Graph层抽象起来其实就三个东西。节点是执行单元类型可以很丰富一个Loop节点调用3.1里的循环、一个Harness调用节点直接调工具不经过推理、一个子图节点嵌套的Graph、一个人工节点挂起等待审批人操作。边是流转规则。最简单的边是顺序边A完成后走B。真正发挥作用的是条件边和并行边。条件边根据状态值决定走哪个分支比如“工单类型是咨询就走自动回复是故障就走告警升级”。并行边则是把一个大任务拆成几个子任务同时跑等所有子任务都完成后再汇合。状态是所有节点共享的上下文。我一般用一个数据类来表达class AgentState(BaseModel): thread_id: str # 会话/工单ID user_input: str # 原始输入 intent: str | None # 意图识别结果 retrieval_results: list [] # 检索结果 draft: str | None # 草稿 final_answer: str | None status: str pending # pending/running/done/failed retry_count: int 04.3 一个典型的生产流程图怎么组织我以“企业IT工单自助助手”为例把Graph的拓扑描述清楚。这不是某个平台的专属配置任何图编排框架不管是自研的还开源的都能映射成这个结构节点类型输入输出后续intent_routerLoopuser_inputintent条件边咨询→knowledge_query工单→create_ticketknowledge_queryHarnessintentretrieval_results顺序边→draftcreate_ticketHarnessintentticket_id顺序边→draftdraftLoopretrieval_results/ticket_iddraft条件边分值0.8→answer否则→human_reviewhuman_reviewHumandraftapproved顺序边→answer驳回→draft_refineanswerLoopapproved_draftfinal_answer结束draft_refineLoopreview_commentrefill_draft回到human_review真正落地时每个节点都要有明确的输入输出Schema。图编辑器比如社区里常见的graph builder工具可以把这些节点拖拽连线生成JSON配置运行时引擎再加载JSON驱动执行。这个方式比硬编码状态机好维护得多业务同学也能看懂流程长什么样。4.4 生产级图编排的三大支柱幂等、重试、超时Graph层把工作流从“代码”变成了“数据”但想让这套流程在生产环境稳定跑必须处理三个工程问题。幂等。节点重跑不能产生副作用。比如“创建工单”这个节点如果网络超时后重试万万不能给用户创建出两个单子。解法是每个节点执行前生成一个execution_id执行结果里带上它重试时先查这个ID是否已经执行过有就直接拿上次结果。重试。节点失败不能直接整单失败。我用的是指数退避第一次失败等1秒重试第二次2秒第三次4秒最多重试3次。超过3次才把状态置为failed并触发审批节点。超时。每个节点要有自己的timeout整个图也要有总timeout。我习惯节点级30秒、图级300秒。超时后的处理是终止图并写入失败快照这样才能保证“系统始终有明确终态”。提到快照还有一个序列化的大坑Graph状态里如果嵌套引用了对象很容易在存储时出现循环引用。热词里那个self referencing loop detected for property mem_memberinfo就是典型的序列化循环引用错误。在Node项目里常见的是对象互相引用导致JSON.stringify爆栈在Python里则是dict互相套引用导致json.dumps失败。我的解法是统一把状态里的子对象转成DTO只留ID和必要字段保存时全量序列化加载时再按ID反查。这个设计从根上避免循环引用。4.5 可视化与调试让流程可回放Graph带来的最大红利是“可视化”。用graph builder类的工具把拓扑渲染出来出问题的时候能直接看到卡在哪个节点。配合状态快照甚至可以做到“回放”把历史某次的完整状态加载回来从那个节点重跑观察到底哪里不对。这个能力我强烈建议大家上线前就埋好。做法也很简单Graph运行引擎里加一层“审计日志”每个节点开始前记录输入结束后记录输出图完成后把整个状态序列化存到专门的快照表里。平时看起来多花了一点存储但出线上问题时你用快照回放定位问题只需要几分钟而不是靠猜或在日志里捞针。5. 三层架构的生产落地一个IT工单助手的完整拆解5.1 场景定义和技术选型理论讲再多不如走一个完整案例。我选“企业内部IT工单自助助手”是因为它兼具了工具调用、多步推理、人工审批、状态恢复等几乎所有Agent生产级要素。技术选型上我用的是一套比较通用的组合Python FastAPI做服务入口Harness层自己封装工具总线Loop层用LangGraph或自研状态机驱动存储层用Redis存临时状态、Postgres存最终工单数据。这套组合的好处是每一层都能独立部署、独立测试也方便后面的横向扩展。5.2 三层职责映射在这个项目里三层的关系非常清楚Harness层注册了sso_verify校验域账号、ticket_create创建工单、sla_query查询服务级别、notify_user发通知等8个工具。每个工具都有Schema声明、参数校验、权限模型、审计日志。Loop层每个子任务单独走一个Loop。比如“理解用户意图”是一个Loop“生成回复草稿”是另一个Loop。每个Loop独立设步数上限大多数只需1到3步上下文互相隔离。Graph层总控整个工单流程。意图识别→检索知识库→创建工单→生成回复→高风险操作转人工→通知用户由一个Graph装起来。5.3 状态对象设计这个项目的状态对象决定了所有节点之间怎么传递数据我直接贴核心结构class TicketState(BaseModel): thread_id: str user_id: str user_input: str intent: str confidence: float 0.0 retrieval_results: list[dict] [] ticket_id: str sla_level: str normal draft: str final_answer: str needs_human: bool False review_comment: str status: str pending # pending/running/waiting_human/done/failed error_info: str State的定义是整个Graph最关键的工程决策我建议一开始就做得宽松一些字段尽量多语义尽量明确宁可暂时用不上也不要等流程跑到一半发现缺字段。因为Graph是数据驱动的状态的Schema一旦定死后面加字段要动到处传播很痛苦。5.4 实现流程要点实现上我按这个顺序推进每一步都有对应的验证第一先把Harness层跑通。在没有任何Loop和Graph的情况下写一个脚本测试所有工具函数能否被正确声明、校验、执行、归一化。这个阶段的目标是“手搓JSON也能调通所有工具”。第二实现Loop执行器。把3.1的骨架代码扩展成带终止判断、上下文截断、异常兜底的完整模块。用几个测试用例验证模型答非所问时步数上限能兜底工具抛异常时Loop能接住并转给模型。第三定义Graph拓扑。把5.2的节点连线写进引擎配置先用模拟数据跑一遍全流程重点看节点间的数据流是否齐全条件边的分支是否正确。第四补生产保障。幂等检查、重试策略、超时策略、状态快照、审计日志这些全部压上。开发环境可能觉得“重试3次”是多余的但线上一定会用到。第五联调压测。先用单请求跑通端到端再做12并发压测观察是否有状态串扰、超时异常、快照写入失败。5.5 效果数据与注意事项上线后我们跑了两周核心指标变化很明显工单自助解决率从55%提升到83%平均处理时长从25分钟降到8.5分钟需要人工介入的比例从65%降到22%Loop平均步数只有2.8步大部分工单在两步内就搞定了图执行失败率2.1%失败原因几乎都集中在下游系统超时有一个问题在灰度期特别突出ticket_create工具在重试时产生了重复工单。虽然我们已经有execution_id幂等设计但下游工单系统没做幂等第一次调用实际成功但超时了第二次调用又创建了一遍。最后我们不得不在工单系统侧加了业务幂等键才算彻底解决。这件事给我一个教训幂等不能只在自己这一层做还要推到下游依赖系统不然永远有漏洞。6. 常见问题与排查技巧实录6.1 生产问题速查表把我在多个项目里遇到的典型问题整理成了一张速查表遇到对应现象直接照着排查效率会高很多现象常见原因排查思路解法harness failed to load plugins插件目录权限、manifest字段错误、依赖缺失看Harness启动日志单独加载插件验证修复目录属主和权限用yaml直接加载测依赖agent execution terminated due to error工具异常未接住、状态字段缺失、API超时查循环日志里最后一次工具调用和异常栈工具调用包try/except校验状态Schema单独设超时self referencing loop detected状态对象循环引用序列化爆栈检查状态里对象的引用关系状态字段改DTO按ID引用不要嵌套对象并发出错/串话Loop实例或消息列表被共享检查是否有全局可变对象请求级隔离状态外置到Redis上下文越来越贵循环不裁剪中间步骤看每次调用的Token消耗统计做上下文截断和摘要压缩沙盒环境过期导致请求失败Agent沙盒镜像没更新看报错里是否提到沙盒版本更新沙盒镜像重建运行环境6.2 三层日志排查法排查问题的时候最怕的是每层日志格式都不一样trace_id对不上。我推进项目的第一件事就是统一日志规范全链路用同一个trace_idHarness的工具调用、Loop的每步循环、Graph的节点执行全部打上这个ID。这样做的收益在线上事故时体现得淋漓尽致。用户说“我的请求失败了”你只需要拿trace_id到日志系统里一查Graph走到了哪个节点Loop在第几步停了Harness最后调用了哪个工具、参数是什么、返回了什么错误。三步就能定位问题不用坐在一起开回溯会议。我还会在Loop的每一步把“模型输入摘要、模型输出完整内容、工具调用参数、工具结果摘要”都记录到结构化日志里字段名统一用prompt_snapshot、tool_call_args、tool_result_summary。以后做性能分析或者模型行为复盘这些数据都是金矿。6.3 关于这套架构我最后想再说的三件事第一不要为了分层而分层。如果你现在只是一个几十分钟跑完的脚本一个Loop加一个简单的Harness就够了硬塞一个Graph层只会拖慢开发节奏。分层是为复杂度和团队协作服务的不是为简历服务的。第二状态管理和可观测性一定最早做不要等上线再补。等到系统已经在线上被用户用了你再想加大日志量、加状态快照就要动正在跑的代码那种压力我经历过真的不想经历第二次。第三这三层本质上是“视角”而不是“强制规范”。你可以把Graph层收敛成非常薄的状态机也可以让Harness层只做参数校验。关键是团队所有人对“哪类问题该在哪个层解决”有一致认知。有了这个共识Agent工程就不再是玄学而是可以设计、可以测试、可以复盘的一门正经工程学科。最后分享一个小技巧无论你最后选了什么框架都记得给Agent的每次“思考”加上日志摘要记录。因为模型的行为不像传统代码那样可预测你只有把它的每一步选择都记录下来才有机会在事后理解它为什么这么做。这是你和Agent系统共事这几年最重要的一笔“技术债投资”。