ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

生产级AI Agent的三层架构:Harness、Loop与Graph实战解析

生产级AI Agent的三层架构:Harness、Loop与Graph实战解析 去年我在做一个内部知识问答 Agent 的时候把所有逻辑——工具调用、提示词拼接、循环重试、状态判断——全部塞进了一个 Python 文件里。前期跑 demo 非常爽写一个函数调通一个工具加两个 if 就能应付新场景。等真正推到生产问题就来了Agent 会反复调用同一个工具不退出某个工具返回了格式奇怪的 JSON 直接导致解析崩溃新同事接手时完全看不懂状态是怎么流转的甚至有一次并发测试时两个会话的结果互相串了。那次重构以后我才彻底想明白一件事生产级 Agent 不能只靠一层“智能循环”打天下它必须被拆成Harness、Loop、Graph三层来设计和维护。这三层不是谁发明的理论而是把 Agent 工程里天然的三个复杂度来源——能力边界、迭代控制、流程编排——分开治理。这篇文章就用我实际踩过的坑把这套三层架构从原理到落地完整讲一遍适合正在从“写 Demo”转向“做产品”的 Agent 开发者参考。1. 先搞清楚三个词Harness 管边界Loop 管节奏Graph 管路径1.1 用一家餐厅的运作逻辑来理解三层职责很多同学搜“Harness、Loop、Graph”时会看到各种互相矛盾的资料其实这三个词描述的是三个完全不同层次的问题。我习惯用一个类比一家餐厅要正常运转必须同时具备厨房设备、出餐节奏、服务流程三样东西。Graph 层就是服务流程。顾客进门、点单、后厨做菜、服务员上菜、结账这是一张有方向、有节点的流程图纸。它决定了客人在某个环节之后下一步能走到哪里。Agent 里的 Graph 层做的就是这个事把“用户提出问题到拿到回答”这整段路径显式地画成节点和边的拓扑结构比如检索节点、推理节点、工具执行节点、最终回答节点。Loop 层就是出餐节奏。后厨不能只做一桌菜就不管了它得循环接单、盯着每道菜的状态菜没熟就回锅上错桌就重做。Agent 里的 Loop 层就是把“当前目标达成没有”作为循环判断条件一遍一遍驱动节点前进直到满足退出条件。Harness 层则是厨房设备和食材权限。厨师只能用现有的设备和食材做菜没有天然气就开不了火冰箱里没有牛肉就做不了牛排。Agent 里的 Harness 层就是那层外壳——它决定了 Agent 能调用哪些工具、装有哪几个 Skill、上下文窗口如何使用、密钥如何注入、哪些系统资源不能碰。1.2 三层的数据边界与调用关系很多人以为 Graph 在最上层、Loop 在中间、Harness 在底层像三层蛋糕一样叠起来。实际它们在运行时是一条链Graph 定义“有哪些节点、每个节点能通向哪里”Loop 是推动器在每个 step 里决定“当前是哪个节点被激活”而节点真正干活时拿到的工具、技能、执行环境全部来自 Harness。贯穿这三层的主线是一份状态对象state。Graph 负责定义状态的结构Loop 负责在循环中更新状态Harness 负责在节点执行时读写状态里被允许的那部分。这里最关键的一条工程纪律是状态字段的读写权限必须由 Harness 统一管控不能让节点随意改全局变量。我在项目里见过太多因为贪图方便把工具输出直接塞进全局 dict 的写法最后并发一上来数据错乱到怀疑人生。1.3 为什么“单层 Agent”会在生产环境失控不拆层也能做出能跑的 Agent但生产环境会暴露三个经典问题。第一是排错困难Agent 出了问题你没法定位是“Graph 选错了路径”还是“Loop 没退出”还是“Harness 没给工具权限”所有可能性搅在一起。第二是迭代困难想优化工具调用逻辑就得动循环代码想改流程就得顺着调用栈摸半天改一处崩三处。第三是成本失控没有独立的终止条件和上下文治理一个不收敛的循环会把 API 预算烧穿。这里我把三种架构方式做个对比架构方式改动一个节点的影响范围并发状态隔离线上排错效率适用阶段单体脚本全局不可控几乎无法隔离靠 log 猜测原型验证单层 Loop循环内可控需要手动加锁能看懂主循环中小规模Harness Loop Graph节点与节点隔离天然隔离每个节点可追踪生产稳定运行我自己现在的判断标准很简单只要 Agent 要服务真实用户、长期迭代、多人维护就必须拆层。这跟代码整洁没有关系它是在为未来的可观测性和成本控制提前铺路。2. Graph 层从 if-else 泥潭到显式状态图2.1 为什么第一步是“画图”而不是“写循环”我接过的 Agent 项目里最普遍的错误是大家上来就写一个while True的大循环然后在大循环里用一堆 if-else 判断应该调哪个工具。这种写法在小场景下很灵活但流程一复杂就变成一团乱麻。我自己重构过的那个问答 Agent当时主流程大概有七个分支每个分支里又有三到四层嵌套判断后来想加一个“用户确认后再执行代码”的环节改了三天还引入了一个新 bug。正确的顺序应该是先画图再写代码。画图不是画什么高大上的架构图就是把用户从提问到拿到结果会经过哪些节点、每个节点有哪几条出路全部写在白板上或 Graph Builder 工具里。这个过程本身就是需求澄清你会发现很多不画图时根本不会注意的边界情况比如“检索没有结果时是重新改写关键词还是直接告诉用户没找到”。Graph 层的第二个作用是让流程可以评审。一个人拍脑袋写的循环别人很难插嘴但一张画出来的状态图产品经理能看懂测试能照着写用例新来的工程师也能在十分钟内搞清楚整个 Agent 的骨架。2.2 Agent 节点的最小设计单位小粒度、可命名、可观测Graph 里每个节点应该是一个动词短语命名的小函数节点内部只做一件事。比如router判断用户意图决定走向哪个分支retrieve从知识库检索相关内容code_exec在沙箱里执行一段生成的代码verify检查上一步输出是否符合要求answer基于已有信息组装最终回答节点粒度太小会觉得碎太大又会回到单体泥潭。我的经验是一个节点如果超过五十行代码或者需要分段注释才能看懂就应该拆成两个节点。每个节点都要能独立测试节点与节点之间只通过 state 通信。举个实际例子我在内部 RAG 项目里把节点的 state 定义为下面这样的结构每个字段都由 Graph 层预先声明class AgentState(BaseModel): user_goal: str # 原始用户目标 retrieved_chunks: list # 检索到的知识片段 tool_results: dict # 各工具的执行结果 current_answer: str # 当前组装中的回答 need_human_confirm: bool # 是否需要人工确认定义了 schema就等于给整个 Graph 画了一条数据红线跑到任何节点都知道自己能从 state 里拿什么、该往 state 里写什么。而且这些字段可以直接被序列化成 JSON 打印进日志排查问题的时候把每个节点的输入输出一比问题出现在哪一跳一目了然。2.3 状态路由的三种典型形态以及它们的成本差异Graph 的边一共有三种常见形态我建议全部支持顺序执行、并行分支、条件跳转。顺序执行最简单节点线性跑完即可。并行分支适合“同时检索多个数据源”这类场景能有效降低时延代价是实现上要考虑多个子任务的结果合并。条件跳转是 Agent 的灵魂比如“检索置信度低于阈值就改写查询词再试一次”。下面是一个简单的路由定义示例graph { start: {next: router}, router: { conditions: [ {if: need_retrieval, next: retrieve}, {if: need_code, next: code_exec}, {if: can_answer, next: answer} ] }, retrieve: {next: verify}, verify: { conditions: [ {if: retrieval_good, next: answer}, {if: retrieval_poor, next: router} ] }, code_exec: {next: verify_code}, verify_code: {next: answer}, answer: {next: end} }这种图结构可以直接用 JSON 存储有版本号能放进 Git 里做代码评审。我把这种配置叫“流程即配置”它的好处是改流程不用改代码上线一个 JSON 文件就够了。2.4 动态子图Skill 和插件为什么要挂在 Graph 上Graph 本身是静态的拓扑但 Agent 的能力必须能扩展。我的做法是允许节点在执行期动态挂载子图——当一个 Skill 被 Harness 装载时它同时会向 Graph 注册一批新节点和路由规则。这样新增一个“PDF 解析”技能时不需要改主流程代码只要把 Skill 的清单文件丢进目录Graph 层在启动时读到并注册对应节点。这个设计带来的额外好处是“局部可降级”。我在生产环境里遇到过某个第三方检索服务不可用的情况因为 Graph 层把该节点单独隔离了我可以直接修改路由配置让所有请求跳过它而不影响 Agent 的其他能力。如果所有功能都堆在一个大循环里这种故障隔离想都不要想。3. Loop 层循环引擎的终止条件与成本控制3.1 最小可用的 agent loop 长什么样Graph 定义的是路径真正推动 Agent 往下走的是 Loop 层。网上有些人会把“Agent 就是一个循环”理解成只要不断调 LLM 就行这其实忽略了一个关键点循环真正的难点不在“转起来”而在“停下来”和“知道自己在干嘛”。下面这段是我的项目中一个最小可用的循环内核async def run_agent(user_goal: str, graph: dict, harness: Harness): state AgentState(user_goaluser_goal) step 0 max_steps harness.config.max_steps # 迭代上限来自 Harness 配置 while step max_steps: node_name graph.resolve_next(state) # Graph 决定下一个节点 if node_name end: break node_fn harness.load_node(node_name) # Harness 装配节点实现 state await node_fn(state, harness) if not graph.is_terminal(state): step 1 else: break return state这段代码虽然短但它把三层的关系体现得很清楚resolve_next是 Graph 的职责load_node是 Harness 的职责而整个 while 结构才是 Loop 本身。写 Loop 层时我建议不要让它承担任何业务逻辑它只负责三件事推进 step、检查终止条件、调用节点。3.2 终止条件的“三件套”迭代上限、token 预算、收敛判断只设置max_steps是远远不够的因为 LLM 可能会在一个死胡同里原地打转反复输出几乎一样的工具调用结果。我的做法是叠加三套终止条件第一迭代上限max_steps是最基本的保险丝作用是防止最坏情况下的无限循环。我一般默认配置 8 步碰到复杂的代码生成任务会放宽到 20 步。第二token 预算。Loop 每次迭代都要调用 LLM一次多轮循环的 token 消耗是指数级增长的必须在 Harness 里配一个全局 token 预算超过立刻终止并返回已有结果。第三收敛判断也是最容易被忽略的。我写了一个novelty_score计算当前 step 的 state 和之前 step 的 state 之间的差异度如果连续三轮差异度都低于阈值说明 Agent 在重复劳动直接停。def should_stop(history): if len(history) 3: return False recent history[-3:] diffs [state_diff(recent[i], recent[i1]) for i in range(2)] return all(d 0.05 for d in diffs)这三个条件缺一不可迭代上限防死循环token 预算防烧钱收敛判断防原地打转。3.3 失败重试与反思循环让 Agent 知道自己错了Loop 层还必须处理一个问题工具调用失败后怎么办。最粗糙的做法是让 LLM 原样重试结果通常是重复同样的错误。我现在的做法分两层。第一层是结构化重试对工具返回的错误做分类超时类错误使用指数退避策略间隔时间按 1s、2s、4s 递增权限类错误直接终止不做无意义的倒腾解析类错误则把“格式不符合契约”这件事单独回传给 LLM而不是把原始报错一股脑丢进去。第二层是反思机制。当某个节点连续失败两次我会插入一个特殊的reflect节点把“当前目标”“已尝试的动作”“失败原因摘要”发给 LLM让它生成一份修正计划再带着修正计划重新进入主循环。这个设计帮我把工具链上的失败率从大约三成降到了接近一成。反思节点的提示词长这样截取核心部分你正在执行的子任务连续失败了。 已知目标{goal} 已尝试动作{actions} 失败原因{errors} 请输出1. 你对失败原因的判断2. 下一步应该尝试的修正动作3. 判断修正动作是继续使用原工具还是换一种思路。3.4 并发场景下Loop 引擎怎么扛住高并发“AI Agent 怎么扛并发”这个话题最近被很多人搜我的答案是循环引擎本身必须无状态化。不能在每个请求里创建一个 Python 对象然后挂在内存中共享否则并发一上来不同用户的 state 会互相覆盖代价极其惨烈。我采用的做法是把 state 序列化成 JSON 快照存入 Redis每次循环的 step 从 Redis 读取快照执行完节点再写回快照。这样循环引擎变成一组可以水平扩展的无状态 worker并发量上来了就加 worker 实例不再被单台机器的内存绑架。同时要加上信号量限流限制同一时刻在途的 Agent 循环数量因为 LLM 的调用配额和沙箱容器数量都是有限资源。实测下来这种“快照化 无状态 worker 限流”的方案比原来每请求一个常驻对象的设计稳定太多。4. Harness 层Skill 装配、沙箱边界与上下文治理4.1 Harness 和 Agent 到底什么关系一个容易混淆的概念搜索热词里有“harness和agent区别”我在这里明确讲一下。Agent 指的是那个能够感知、决策、行动的智能主体——它的核心是 LLM 和围绕 LLM 的推理逻辑。Harness 则是指承载这个智能主体运行的工程外壳工具注册表、Skill 管理系统、上下文窗口分配器、密钥注入器、沙箱环境。可以这样理解Agent 是灵魂Harness 是身体Agent 负责“想”Harness 负责“装”和“护”。一个 Agent 可以对应多个 Harness 吗可以。同样的智能核心可以装在不同的 Harness 里应对不同场景内网环境用一个受限 Harness公网环境用一个能力更全的 Harness。反过来一套 Harness 也可以承载多个 Agent 实例通过配置区分装载哪些 Skill。在项目里我把 Harness 设计成纯配置驱动——每个部署环境目录下的harness.yaml决定装载什么代码本身不感知环境差异。4.2 Skill 系统的装载语义显式依赖胜过隐式魔法Harness 层最重要的功能是管理 Skill。一个 Skill 不是一个提示词文件而是一个自包含的能力包我的组织目录长这样skills/ pdf_parser/ skill.yaml # 元信息名称、版本、依赖、声明注入的工具名 prompt.md # 调用此 Skill 时拼接进 system 的提示词 tools.py # 真实执行逻辑PDF 解析、表格抽取等 web_search/ skill.yaml prompt.md tools.py启动时 Harness 会扫描skills目录读取每个 Skill 的skill.yaml完成三件事将prompt.md注入系统提示词、将tools.py里暴露的注册函数加入工具表、把 Skill 声明的依赖比如外部 API 的 key注入执行环境。这里我踩过一个深刻的坑有一版 Skill 系统用了全局变量读取工具结果两个 Skill 的变量名撞了一个 Skill 的工具被另一个 Skill 意外覆盖。后来全部改成显式依赖注入接口参数从哪传就从哪拿不再允许任何“隐式全局状态”。4.3 工具返回值的契约设计机器可解析优先级最高Harness 层的另一个关键职责是定义工具返回值的格式。LLM 调用工具后返回的内容是它理解世界的窗口如果这个窗口的数据一团乱麻LLM 再聪明也没用。我自己定了一套工具返回契约所有工具统一返回 JSON{ status: success, data: { ... 核心结果 ... }, error: null, meta: { cost_ms: 230, source: internal_db } }失败时{ status: error, data: null, error: { type: timeout, message: service timed out, retryable: true }, meta: { cost_ms: 5002, source: plugin_registry } }这个契约的价值在于LLM 可以准确地从status字段判断成败从error.type判断该不该重试从error.retryable判断是否值得做指数退避。而不是让 LLM 去海量的自然语言报错里找线索。我还会在系统提示词里放一个工具返回值的样例教模型优先读取结构化字段而不是凭“感觉”解读工具结果。4.4 沙箱边界与上下文治理Harness 是可信壳层Harness 还是安全边界。我的原则是LLM 永远不直接接触真实系统资源。工具实际上跑在独立的受限容器里网络访问走白名单代理数据库连接用最小权限账号密钥通过环境变量注入工具进程而不是出现在任何提示词或日志里。这样即使 Prompt 被注入恶意指令攻击者也拿不到宿主机的真实权限。上下文窗口治理同样放在 Harness 层处理。我会给一段对话分配固定的 token 预算并按比例拆分系统提示词约占 20%Skill 描述占 15%工具返回结果占 30%历史对话占 25%剩余 10% 留给生成空间。超过预算时执行摘要压缩而不是简单粗暴地从开头截断。老实说因为 LLM 窗口有限“压缩什么、保留什么”本身就是动态策略Harness 会依据当前节点类型保留最相关的历史片段比如执行到code_exec节点时就多保留与数据格式相关的历史。5. 三层联动的生产实践一个可落地的 Agent 项目拆解5.1 拿一个有代表性的需求做完整拆解为了让前面的理论落到地上我完整还原一个最近在做的项目企业内部知识问答 Agent支持联网检索、内网文档检索、代码执行三种能力需要部署到内网服务器。选型上我直接按三层架构落地Graph 层用 JSON 配置的静态图Loop 层用有状态快照化循环Harness 层做纯配置驱动的 Skill 装载。这套组合看起来不惊艳但非常耐用。Graph 层只有一个router节点做意图分流后面挂三条分支联网检索、内网检索、代码执行。每条分支结束汇入answer节点。Loop 层的max_steps设成 8token 预算按 6000 控制收敛判断阈值 0.05。Harness 层装配三个 Skillweb_search、internal_rag、code_sandbox。5.2 从 Graph 定义到 Loop 运行的核心实现启动流程是Harness 先扫描 Skill 目录完成工具注册Graph 层读取 JSON 配置把 Skill 注册的节点挂到图上Loop 层启动一个 FastAPI 服务每个请求进来后创建独立的 state 快照进入循环。核心代码片段如下# startup.py 简化示意 harness Harness.from_config(config/harness.yaml) graph Graph.load_json(config/graph.json) graph.register_node(web_search, harness.get_skill(web_search).node_fn) graph.register_node(internal_rag, harness.get_skill(internal_rag).node_fn) graph.register_node(code_sandbox, harness.get_skill(code_sandbox).node_fn) app FastAPI() app.post(/agent/run) async def handle_request(req: UserRequest) - dict: snapshot RedisSnapshot.create(AgentState(user_goalreq.goal)) result await run_agent(snapshot, graph, harness) # 复用第3节的 loop return result.to_dict()这里有个细节值得注意run_agent接收的是snapshot而不是裸 state因为并发场景下我们必须保证每个请求的 state 互不干扰。快照的序列化内容就是第 2 节定义的AgentState这再次说明三层架构是同一个数据模型串起来的。5.3 内网部署与“插件加载失败”的完整排查链路内网部署是生产实践里最容易出问题的环节。不少人在开发机上跑得好好的一搬到内网服务器Harness 启动时报failed to load plugins。这个报错的排查链路我完整走了一遍记录如下第一先看 Harness 启动日志里的插件名。failed to load plugins是一个泛指真正原因都在前面的具体报错里比如“libgomp.so.1: cannot open shared object file”。第二如果报错指向某个动态库缺失用ldd 二进制文件查看依赖常见的坑是内网机缺少编译工具链很多 .so 库没装上。第三检查插件目录权限Harness 进程如果以低权限用户运行而插件文件是 root 所有加载必然失败。第四对比开发机和内网机的 Python/C 运行时版本特别是 glibc 版本不一致导致的兼容性问题。我的最终解法是把所有依赖打包成离线 wheel 包在内网用pip install --no-index --find-links安装对于涉及编译的原生依赖直接交叉编译成静态库再分发插件目录放到统一的可写路径并让 CI 在构建时自动生成一份环境校验报告跟 Harness 日志对比省得每次手工逐个排查。5.4 版本回退与平滑升级上线三天的第二次教训那次项目上线第二天新加入的web_searchSkill 导致router节点意图判断准确率骤降。原因很典型新 Skill 的prompt.md在系统提示词里加入了大量搜索技巧的说明把路由器对“能否直接回答”的判断带偏了。好在我们把 Graph 配置和 Skill 清单都做了版本化每次发布生成一个 manifest 文件记录各项的 commit 号。发现回归后执行了一次一键回退Manifest 回退到前一天版本Graph 配置和 Skill 目录同时还原Loop 配置不变整个过程大约两分钟。这次教训让我养成了一个习惯任何 Skill 或 Graph 改动都必须先在隔离环境跑一遍回归测试集里面有二十个常见的用户问题专门验证核心路径不回归。没有这套回归我没有勇气让 Agent 在无人看守的情况下跑在生产上。6. 生产环境最容易踩的坑附完整排查思路6.1 死循环与成本失控账单是最诚实的报警器死循环在 Agent 生产环境里不算罕见尤其是“Agent 反复调用同一个工具”这个症状最常见。我排查时的第一步是打开循环日志观察每个 step 的工具调用记录。如果连续三轮出现了完全相同的tool_results内容基本可以确认 Agent 陷入了重复劳动的怪圈。根因往往是终止条件只设了max_steps但缺乏收敛判断LLM 在一个“反正还没达到最终目标”的状态下不断重复。解决的路径我给过在 Loop 层加novelty_score连续 N 轮无新信息就强制终止并返回当前最优结果。还要给工具调用加上“相同参数调用次数上限”同一个工具同一个参数最多执行三次超过就标记为异常路径。6.2 Graph 状态变量污染并发一上来就“串数据”有一次压测时用户 A 的检索结果出现在用户 B 的回答里。第一次遇到我以为是缓存问题查了半天发现是 Graph 层的 state 对象被多个循环实例共享了。当时代码里有一个模块级的全局state {}当作数据总线所有循环实例都往里写并发时互相覆盖。排查链路是这样的先确认问题只在并发时出现单线程跑怎么都对这就基本锁定了共享状态。接着用日志把每个请求的 state 快照 ID 打出来发现两个请求的 ID 相同直接实锤。修复方法是把 state 改成 per-request 的独立对象并在 Redis 快照层做隔离。这个坑给我们的教训是Graph 层定义的数据总线必须显式绑定到单个循环实例永远不要用模块级可变对象。6.3 工具输出解析失败LLM 不总是规规矩矩吐 JSON工具返回契约制定得再好LLM 在调用工具时也可能不按契约走。实测中经常遇到的行为工具返回了合法 JSON但 LLM 在调用时误把 JSON 当成自然语言在最终回答里把原始 JSON 整个复述出来。还有一种情况是工具返回内容里包含了非法字符比如内网文档里的二进制内容导致 JSON 解析直接崩溃。我的处理办法是三层防御。第一Harness 侧用一个宽松解析器先尝试json.loads失败则剥掉 markdown 代码块符号再定位第一个{到最后一个}截取片段。第二工具进程内部在返回前对内容做清洗把无法用 UTF-8 编码的字符全部替换为占位符。第三LLM 侧的提示词里明确写着“工具返回的status字段是判断依据不要在回答里直接粘贴 JSON”并在每个会话开头放一段交互样例。6.4 Skill 命名冲突加载顺序决定行为这是玄学但要命Skill 多了以后工具命名的冲突几乎不可避免。我们曾有两个 Skill 都注册了search工具一个走内网文档一个走外网搜索。Harness 按目录字典序加载结果“外网搜索”版本先加载内网场景下所有请求都跑到了外网搜索返回了一堆不该出现的内容把审核的同学吓了一跳。排查链路第一步看 Harness 启动日志里工具注册表的顺序第二步检查每个 Skill 的skill.yaml里声明的工具名。修复方法有两个一是工具命名增加命名空间前缀比如internal_doc_search和web_search彻底避免冲突二是启动时加一道“工具名冲突检测”发现重复注册直接报错阻止启动而不是静默覆盖。我强烈建议两个都做前者为了清晰后者为了防止未来再犯。6.5 上下文被粗暴截断导致的“失忆”Agent 突然答非所问长会话场景里“Agent 突然不记得前面讨论过什么”不一定是被 Skill 或工具影响的很可能是历史对话在窗口压缩时被粗暴截断。我之前的环境里压缩逻辑是超出预算就把最早的消息删除结果用户在前面提到的偏好、上下文约束全被清空了Agent 后续回答越来越离谱。排查链路的突破口是看 Harness 的上下文压缩日志里面记录了每一步丢掉了哪些消息。修复方案是分层摘要把太旧的历史消息先让 LLM 生成长度可控的摘要摘要进入新的“记忆区”仍有价值但太长的原始消息写入外部存储。同时预留“关键信息保护区”凡是用户明确标注“记住”的信息压缩时一律不丢弃。这套机制上线后长会话场景的答非所问率明显下降。我现在接任何 Agent 项目第一件事永远是让团队坐下来把 Graph 画出来——不画到每个节点有明确输入输出、每条边有条件说明不会让任何人开始写代码。然后再讨论 Loop 的终止条件和预算上限最后才决定 Harness 里要装什么 Skill。这个顺序至少三次救了我的项目因为我见过太多团队上来就调 Prompt、往一个大循环里塞工具最后陷入“修了这个又坏了那个”的恶性循环。最后分享一个对我最管用的小技巧在 Graph 的每个节点出口打一份结构化日志内容包括节点名、输入 state 的关键字段、输出 state 的关键字段、耗时和 token 消耗。这个日志文件就是整个三层架构的体检报告哪一层有问题翻开日志五分钟就能定位。不要等到出故障了才想到观测生产排错里最贵的永远是定位时间。
返回列表