AI Agent Harness可观测性实战:用LangSmith拆解黑箱

发布时间:2026/10/22 3:12:52
AI Agent Harness可观测性实战:用LangSmith拆解黑箱
做过一段时间 Agent 开发的人基本都经历过同一个场景本地调试的时候一切正常一进集成环境就开始失控。Agent 的输出越来越离谱工具调用反复重试上下文越来越长最终要么超时、要么给一个看起来挺通顺但完全没有依据的答案。这种时候你打开日志看到的是几百行各自孤立的打印记录谁调用谁、哪一步花了几秒、token 到底烧在哪里完全拼不起来。这也是为什么我越来越觉得AI Agent Harness Engineering 的核心难点不在“让模型变聪明”而在“让模型的运行过程变得可观测”。这篇文章我想用实际做过的项目经历聊聊怎么用 LangSmith 把 Agent 的“黑箱”拆开实现真正的全链路追踪与可视化。这里先解释一下我理解的 Harness Engineering。它并不是在讨论某个具体框架而是指“包在模型外面的一整套运行装置”上下文怎么组装、工具怎么注册、循环怎么控制、错误怎么恢复、结果怎么评估。模型本身是一个黑箱而 harness 如果做得不好会让这个黑箱变得更黑。可观测性不是一个加分项它是 harness 里跟“控制流”同等重要的一个模块。文章会从 harness 的职责边界讲起然后拆解 Agent 运行链路里最容易“黑”掉的四个环节再深入 LangSmith 的数据模型和埋点接入方式最后用一个真实的排障过程演示怎么靠追踪数据定位问题。内容尽量给到可以直接复用的思路不管你是刚接触 Agent 开发还是已经在维护线上智能体系统应该都能用得上。1. Harness Engineering 到底在工程化什么模型之外的另一半工作1.1 从“能跑通”到“跑得稳”先给 harness 划个边界很多人第一次接触 Agent以为核心工作就是写 Prompt 和调模型接口。等真正上了规模才发现模型调用只是整条链路里很靠后的一步。我习惯把智能体系统分成两层一层是模型层负责把指令变成 token 再变回文本另一层就是harness 层负责决定模型在什么条件下、以什么顺序、利用哪些信息来完成这件事。举一个最简单的例子。一个能查天气、查航班、写行程建议的 Agent看起来功能不复杂但拆开看就有很多需要 harness 处理的细节用户说“明天去上海出差帮我看看天气和航班”系统到底先查天气还是先查航班两个工具返回的数据在哪个环节合并如果航班查询失败是直接告诉用户还是换一个数据源重试模型返回的 JSON 格式里如果把字段名写错了谁来纠正这些问题没有一个是模型自己解决的全是 harness 的事。所以我的定义是harness 是围绕模型调用的“骨架系统”它不负责思考但负责让思考能够发生并能够被验证。这是一个很关键的心态转变——你在开发 Agent 时大部分时间维护的不是 Prompt而是这层骨架。1.2 Harness 的四大模块上下文、工具、控制流、记录我在实际设计 harness 时通常会把它拆成四个模块每个模块都有自己独立的可观测性关注点。第一个是上下文组装模块。它负责把系统提示词、用户输入、工具返回结果、历史记忆拼装成模型真正看到的内容。这个模块最容易出现的问题是“上下文污染”比如把上一次对话的完整原文原样塞进去结果模型分不清哪些是当前任务的约束哪些是历史记录。追踪时需要重点记录的是每次组装后的实际内容长度、包含了哪些片段、有没有被截断。第二个是工具注册与调用模块。工具在这里被封装成统一的接口包括名称、描述、参数 schema、执行函数、返回结构。这个模块的黑箱点在于模型看到的工具描述和你代码里实际执行的函数之间可能存在偏差。比如描述里说参数city是必填但模型经常不填harness 就得有默认值的兜底逻辑。追踪时要记录的是模型原始的工具调用参数、经过 harness 补全后的最终参数、以及工具执行后的结构化返回值。第三个是控制流模块。Agent 的循环、分支、终止条件都在这里。你设置的最大迭代次数是多少模型连续两次生成相同结果时要不要提前终止某个工具连续失败三次是不是应该切换策略这些逻辑决定了 Agent 的整体行为边界。追踪时需要记录的是每一轮循环的判定依据、当前轮次、累计消耗、以及终止原因。第四个是记录与评估模块。也就是把我们上面说的这些追踪数据落库、聚合、可视化并跟人工反馈、自动化评估打通。这个模块恰恰是很多人最容易忽略的总觉得“先把功能做完再补监控”结果功能上线之后出问题完全无从下手。1.3 为什么追踪不是附加项而是 harness 的第四个模块我见过不少项目把追踪当作“上线前再接入”的工具。但在 Agent 这种非线性系统里这个顺序必须反过来。原因很简单普通程序的执行路径是确定的if-else 分支再多也能靠单步调试覆盖而 Agent 的执行路径是模型根据输入动态生成的同一个问题今天走这条路明天可能走另一条路日志只能记录“发生过的”追踪才能把“发生过的”和“为什么发生”对应起来。把追踪当作第四个模块还有个好处它会倒逼你把前三个模块的接口统一。比如当你给工具的每个调用都要记录参数时自然就会去规范工具的参数 schema当你给每一轮循环都要记录终止原因时自然就会把终止条件从散落的 if 语句收敛成明确的判定函数。这不是额外工作这是让 harness 本身变得可维护的一次重构。2. Agent“黑箱”是怎么炼成的四类常见的失控环节2.1 子任务递归一个动作可能长出一整棵调用树Agent 系统里最让我头疼的就是模型为了完成一个目标自己把任务拆成多个子任务然后递归调用。这不是框架强制的而是模型在“plan-and-execute”模式下自然产生的行为。比如用户问“帮我安排下周的工作计划”Agent 可能先调用一个日程工具再调用优先级排序工具然后又为了确认某个项目的依赖关系去查文档库整个过程像一棵不断生长的树。这种递归会造成两种黑箱。一种是失控的深度模型在某个分支上反复细化完全偏离用户原始问题。另一种是资源黑洞整棵树的 token 消耗呈指数增长但你从单次日志里根本看不出来因为每次调用看起来都只是“正常的一次函数调用”。追踪系统必须能把这种父子调用关系记录成树形结构这样你一眼就能看出哪棵子树长得过分。我见过一次排障某 Agent 在 80 秒内完成了 40 多次子任务调用其中 30 次是在同一个分支上的自我修正。从顶层看只是一个plan_trip调用点开之后才发现底下藏了一棵巨大的“修正树”。2.2 隐式回退和容错逻辑故障被静默吞掉了另一个常见的黑箱来源是 harness 里的容错逻辑。为了保证稳定性很多系统会在工具调用失败后自动重试、在模型输出格式不对时自动解析修正、在某厂商接口报错时自动切换到备选模型。这些逻辑本身是好的但如果它们不留下任何痕迹就会变成非常隐蔽的黑箱。我经历过的典型场景是线上 Agent 突然响应变得很慢但所有业务日志都是正常的。后来才发现因为主模型接口连续超时harness 自动切换到了备选模型而备选模型的生成速度只有主模型的四分之一。如果没有 trace你只会看到“耗时变长了”完全不知道是回退逻辑触发了。所以我在做追踪埋点的时候会把所有回退、重试、降级行为都显式记成一个单独的 run 或事件并且打上fallback、retry这样的标签。2.3 工具返回结构漂移模型以为自己拿到的是 A实际拿到的是 B工具接入初期返回结构通常是稳定的。但如果工具是调用外部服务返回的数据结构可能悄悄变化某个字段从必填变成可选某个枚举值被新增了一个选项某个时间字段的格式从字符串变成了时间戳。模型本身对结构变化非常敏感尤其是当 Prompt 里没有明确说明字段格式时。这种问题的可怕之处在于它往往不直接报错而是让模型“理解错”。比如一个查询接口原本返回status: ok某天开始返回status: 0模型看到 0 可能以为是“正常”也可能以为是“停机”具体表现完全取决于模型当时的猜测。这时候如果没有结构化的追踪数据你很难判断问题的根源是工具返回变了还是模型理解偏了。把每次工具调用的原始返回和模型真正收到的上下文内容都记录在 trace 里是定位这类问题的关键。2.4 记忆膨胀与上下文漂移跑着跑着就变成了另一个 Agent还有一种更隐蔽的黑箱是长期运行导致的行为漂移。多轮对话的 Agent 往往会维护一个记忆池把历史关键信息存起来。问题在于当记忆积累到一定程度或者某条误写入的记忆占据了过高权重模型的行为就会悄悄偏离最初的设定。这类问题在单次运行里完全看不出来必须把多次 run 放在一起对比才能发现趋势。比如某个客服 Agent 上线第一周好评率正常第二周开始频繁给出超出权限的承诺。对比追踪数据后发现记忆池里某条错误的“用户是 VIP”信息被反复写入导致模型在后续每一轮都基于错误前提作答。所以记忆模块的读写操作也应该成为追踪的重点对象每次写入的 key、value、来源、置信度都要可回溯。3. 追踪系统拆解LangSmith 的数据模型与传播机制3.1 Run、Span、Trace一次任务从上到下的三种切片在 LangSmith 的模型里一次 Agent 运行的完整记录叫作 Trace它本质上是一棵树。树的根节点是最外层的一次链式调用子节点是这期间发生的每一个内部步骤。上图中的“黑箱”结构在 LangSmith 里可以被拆平为三层概念Trace对应一次完整会话/任务Run对应一次最小的执行单元Span对应一次带时间跨度的分段执行。我最初用这个概念时容易混淆后来总结了一个好记的角度Trace 是“用户视角”Run 是“代码视角”Span 是“时间视角”。用户只关心一次提问最终有没有被回答所以一个 Trace 就是一个完整回答代码关心的是执行细节所以每个函数调用可能对应一个 Run时间线关心的是一段时间内发生了什么所以同一个 Run 的持续过程也能拆成多个 Span。三者不是互相独立的数据而是同一棵树的不同观察维度。具体到 LangSmith 的实现一个 Run 的数据大致包含以下字段字段含义排障时的用法run_type运行类型如 llm、chain、tool快速看出这步是大模型调用还是工具调用nameRun 名称如 query_documents用于聚合统计某一类操作的总耗时inputs / outputs该步生效的输入输出核对模型看到的上下文是否完整start_time / end_time开始与结束时间计算单步耗时定位卡点metadata自定义元数据记录模型版本、重试次数、回退原因等error异常信息没走正常输出的步骤是否报错token_usagetoken 消耗明细分析成本与上下文长度趋势3.2 上下文标识如何贯穿整个调用过程追踪要有效前提是同一次任务的所有子调用能关联到同一个上下文标识。LangSmith 的做法是给每一次顶层运行分配一个 trace id然后通过参数透传或者异步上下文变量把这个 id 传播到所有子调用里。这里我踩过一个很深的坑在异步代码里如果单纯用全局变量去放 trace id并发场景下会串号。A 请求的子调用被记到了 B 请求的 trace 下面排查的时候所有数据都对不上。后来我统一采用“显式传参 上下文容器”的方式把 trace 上下文对象作为一个显式参数传给每一个 harness 内部的函数确保子调用能沿着调用链一路携带父 run id。LangSmith 的 SDK 在这个环节做了不少封装。如果你用的是官方的装饰器或包装器它会自动处理这种上下文传播但如果你是像我一样自研 harness强烈建议在代码里保留一条显式传递的路径而不是完全依赖隐式变量。这样即使 SDK 某些版本行为有变化你的底层逻辑也不会崩。3.3 一个 Run 里真正值得记录的数据很多人以为埋点就是把函数名和入参记一下其实这只是最基础的一层。我在给团队的 harness 制定埋点规范时明确要求三类数据必须进 Run第一类是模型看到的真实内容而不是原始入参。比如一个工具的真实返回值是{code: 0, data: [...]}但经过 harness 格式化之后模型看到的是“查询成功共 3 条结果第一条……”。这两个信息都要记因为模型是基于后者做决策的排查时必须还原模型视角。第二类是决策点原因。任何影响运行路径的 if 判断都应该考虑记录下来。比如“重试次数超过 2切换工具”这条路径需要记录切换原因、重试次数、新旧工具名称。这些元数据可以用 metadata 字段携带也可以在 Run 的 inputs 里加一个字段。第三类是异常与降级标记。正常输出当然要记但“发生了异常、然后被兜底恢复”的过程更要记。我通常会用一个has_error这样的布尔字段再加一个error_reason描述即便后续流程恢复成功这些信息也能在统计时暴露系统的脆弱点。3.4 从单条 Trace 到 Project、Dataset、Feedback单条 Trace 只解决“这次运行发生了什么”的问题真正对 harness 有改进价值的是把大量 Trace 聚合起来看趋势。LangSmith 里我用得比较多的有三个能力。Project负责把同一类运行聚合在一起。比如我通常会为“生产环境主 Agent”“测试环境回归集”“某个新工具的试探性运行”分别建不同的 Project避免不同环境的 trace 混在一起导致指标失真。Dataset负责沉淀回归测试集。当你在 trace 里看到一个高质量的运行样本时可以把它转存为一个 dataset 样本后续每次 harness 改造后就拿这批样本去回归比较改造前后的输出质量。Feedback负责承接人工评价。用户/标注人员可以对某条 trace 打分或打标签比如“答案正确”“答案编造”“工具选错”。这些反馈会跟 trace 关联起来后续可以按反馈标签筛选 trace集中分析某类问题的高发原因。4. 动手接入把自研 harness 变成 LangSmith 的一条完整链路4.1 初始化 SDK环境变量与客户端接入的第一步是把 SDK 装好并完成初始化。这个过程不复杂核心是把接口地址和认证信息配好但我建议不要直接写死在代码里而是放到环境变量中原因很简单非生产环境不应该往生产项目里写数据项目隔离靠的就是不同的 API Key。# 配置基础信息实际使用时长这样设置 LANGSMITH_API_KEY你的认证密钥 LANGSMITH_PROJECTagent-harness-prod代码里的初始化也就几行from langsmith import Client client Client()这里有一个容易踩的坑Client 默认读取环境变量但如果你在服务启动之后才设置环境变量可能会读到空配置。我建议在服务启动入口的最早阶段就完成初始化并且在日志里打一条“client.config”的摘要信息确认当前连接的项目名和地址是对的。4.2 用装饰器给顶层入口埋点如果你的 harness 是自己写的最快的方式是在顶层入口函数上加上 trace 装饰器。LangSmith 的traceable装饰器能把函数变成一个 Run 节点并自动处理子调用的传播。用一个非常简化的例子来说明from langsmith import traceable traceable(run_typechain, nameagent_entry) def agent_entry(user_query: str): # 查知识库 docs retrieve_docs(user_query) # 组装上下文并调用模型 answer generate_answer(user_query, docs) return answer traceable(run_typetool, nameretrieve_docs) def retrieve_docs(query: str): return {documents: [...], chunk_ids: [c1, c2]} traceable(run_typellm, namegenerate_answer) def generate_answer(query: str, docs: dict): # 这里实际上会调用外部大模型接口 return 这是最终的回复这样一次agent_entry调用会在 LangSmith 的界面上形成一棵三层树根节点agent_entry底下挂着retrieve_docs和generate_answer两个子节点。如果你还想看模型内部的输入、输出、token 数可以在generate_answer函数里把模型调用的返回结果完整写入 outputs并记录 raw token 信息。4.3 给自定义工具和大模型调用分类型埋点埋点除了覆盖顶层入口还要覆盖到具体的工具和模型调用。原因很简单如果只给顶层入口埋点你只能看到“整体耗时 10 秒”不知道其中模型调用占了 7 秒、工具执行占了 2 秒、解析和重试又占了 1 秒。只有按 run_type 把每类步骤分开标记聚合统计才有意义。我在实际项目中会给三类函数分别加上装饰器大模型调用统一用run_typellmname 统一为“模型名用途”比如glm4_flight_extract、claude_summarize。这样后续可以按模型名筛选分析不同模型在不同任务上的时延和效果差异。工具执行统一用run_typetoolname 用工具名。对于有副作用的工具比如写数据库、发消息我会额外加一个metadata{operation: write}方便后续做审计。内部的处理步骤比如“解析模型输出”“组装上下文”“格式校验”用run_typechain或run_typechain变体。关键是把你的 create/update span 这类逻辑做成一个小函数而不是散落在各处。如果你的 harness 是用标准流程封装的也可以直接用 SDK 提供的模型包装器把底层客户端包起来比如wrap_openai这一类。但是我更习惯于手动埋点因为自研 harness 里往往还有一层自己的重试和回退逻辑只有手动埋点才能把这层逻辑也暴露到 trace 里。4.4 手动创建子 Run处理 SDK 不好覆盖的边界场景有些边界场景用装饰器不太好覆盖比如一个函数内部同时调用了多次模型或者某段逻辑是循环执行、次数动态变化。这种情况下我会选择手动创建 Run。import uuid from langsmith import Client client Client() def run_manual_chain(question: str): run_id uuid.uuid4() client.create_run( run_typechain, namemanual_chain, inputs{question: question}, # 如果这是某个顶层 run 的子节点需要传入 parent_run_id # parent_run_idparent_run_id, ) try: result {answer: 这是模拟结果} client.update_run( run_idrun_id, outputs{result: result}, end_timeNone, # 不显式传 end_time 时SDK 会自动补当前时间 ) return result except Exception as e: client.update_run(run_idrun_id, errorstr(e)) raise手动埋点的核心是自己管理 run_id 的生成与传递。一定要保证同一个子链里的 create 和 update 用的是同一个 id并且子 Run 要正确挂到父 Run 下。最容易出的问题是在循环里每个子任务都生成了新 id却没有把父 id 串联起来最终 trace 变成一堆孤立节点。4.5 接入时最常见的三个问题接入过程整体不复杂但我还是想把常见问题集中说一下。第一个问题是异步环境下的上下文丢失。用装饰器时如果内部创建了新的 task子 task 的 trace 信息可能没法自动挂到父层。我的应对是尽量用显式传参的方式把 trace 上下文对象传递到每个子函数里。第二个问题是IO 阻塞导致的 trace 异常。埋点本身要写外部存储如果存储端响应慢会造成 harness 的额外延迟。我会在埋点外层套一个超时控制比如单次上报超过 2 秒就降级为丢弃而不是无限等待。第三个问题是敏感数据入 trace。后面第 7 节会专门展开但这里先提个醒接入的时候就要规划脱敏字段不要等出了问题再清洗。5. 可视化面板的正确打开方式指标与视图的读法5.1 时间线视图把“黑箱”摊平成一张调度图埋点做完之后LangSmith 的 trace 详情页会把一次运行展开成一张带时间轴的树状视图。我最先看的永远是这张图因为它能快速回答“时间到底花在哪里”这个问题。正常形态的 trace 应该是顶层一个根节点下面按顺序展开几个子节点每个子节点的时间跨度都很清晰。异常形态的 trace 往往有几种典型模式一是某个子节点时间特别长比如某个工具调用花了 20 秒而阶段本身只要 1 秒二是同一个节点反复出现比如retry_parse连续出现十几次说明模型输出格式反复不达标三是树的层次过深说明模型在一个子任务上不断自我递归。看时间线视图有一点要提醒不要只看平均耗时要看分布。某个节点平均 1 秒但它偶尔会跑到 30 秒这种数据在平均值里根本看不出异常。我一般会配合延迟的百分位p50/p95/p99来看而不是只盯均值。5.2 关键指标Token 消耗、时延、调用次数、反馈LangSmith 的统计页会给出大量指标但不是所有指标都值得天天盯。我给自己定的核心看板指标是以下这张表指标看什么异常信号Token 消耗单次请求的总 token 数、输入 token 比例输入 token 占比持续走高说明上下文膨胀时延按节点聚合每个 run_type 的平均与百分位时延单节点 p99 突然变长优先怀疑外部依赖调用次数工具调用次数、模型调用次数、重试次数调用次数异常多说明循环控制有问题反馈均分人工对 trace 的评分趋势评分下行往往早于业务指标暴露问题Token 这项我特别想多说一句。很多团队只看“总 token 消耗”但这个指标会掩盖上下文膨胀问题。正确的看法是拆开看如果输入 token 占了 90% 以上说明模型每轮都在接收大量历史信息一旦超过模型的上下文窗口要么被截断、要么资源耗尽。这种情况在 trace 里表现出来就是每一轮模型调用的 inputs 越来越大而 outputs 保持很小。5.3 用参考 trace 建立“行为基线”对比功能是我认为 LangSmith 里最有价值但最少被用好的能力。当你发现某条 trace 行为异常时可以拿它与一条“表现正常”的 trace 做对比。我通常在 harness 每次发布后都会从线上采样一批高质量 trace 存为参考基线。这些基线不一定是最新的而是代表“我们希望系统保持的状态”。每当模型反馈、工具结构或 prompt 有变动时我都会拿新 trace 和基线 trace 做逐节点对比看哪些路径变了、哪些节点的耗时或输出结构变了。举个例子某次我把一个工具的返回结构从二维数组改成了对象数组光看代码觉得没问题但跑完 trace 对比后发现模型把新结构里的记录数误读成了字段值连续三次重试。如果不是做了 trace 对比这种问题要等用户反馈才能暴露成本高得多。5.4 把 trace 变成数据集从观测到回归测试Traces 的价值不只在于“事后看”还能变成“事前防”。LangSmith 允许把某条 trace 转存成 dataset 的一个样本。我通常会把线上运行中表现得特别好和特别差的样本都存下来特别好的作为回归黄金样本特别差的作为边界问题样本。这样一来每次修改 harness 之后我先拿这批历史样本在项目里跑一遍。如果原本能正常回答问题的样本在新版本里挂了或者原本会出错的样本现在不报错了就能很敏锐地发现行为变化。追踪数据和评估数据打通之后harness 的每次改动就有了一组可对比的基线而不是靠感觉上线。6. 复盘一次多轮 Agent 空转的黑箱定位全过程6.1 现象看起来很顺畅就是没有结果我印象很深的一次排障是某团队接了一个文书处理 Agent 的新需求。Agent 需要在多轮对话里逐步收集用户资料再调用一个复核工具生成最终文书。现象很诡异线上的 Agent 没有报错用户也说“聊得挺顺畅”但就是经常聊到最后没有输出结果用户只能重新开始。业务方第一反应是“大模型不听话”但我拿到 trace 数据之后发现根本不是这么回事。这个案例给了我一个很深的教训Agent 的很多问题表面上看是模型问题实际上 harness 层早就在埋雷了。6.2 第一步从 trace 列表找异常 run排查的第一步不是去看某个具体节点而是先从 trace 列表里筛选“耗时异常”的运行。我在 LangSmith 的项目列表里按耗时排序把耗时最长的几十条 trace 点开看。很快发现一个规律所有异常 trace 的根节点都特别大执行时间在 5 到 15 分钟之间而正常 trace 基本都在 1 分钟以内。这说明问题不在单次模型调用而在整个链路的循环控制上。点开一条异常 trace时间线视图立刻暴露了第一个可疑点validate_document这个工具节点在整条链路上反复出现了 20 多次而且每次都紧接着一次generate_answer。正常情况下这个工具最多调用 3 次超过这个次数就说明循环没有正确退出。6.3 第二步时间线定位卡点进一步观察时间线我发现每次validate_document的执行时间都很短几百毫秒到一秒之间但是紧接着的generate_answer模型调用却一次比一次长。第一次模型调用花了 4 秒第 10 次已经涨到 18 秒。这个趋势非常典型上下文在每一轮循环里持续膨胀导致模型处理时间不断变长。从 token 数据上也验证了这一点单次运行的输入 token 从第一轮的 4000 左右涨到最后一轮的接近 3 万。每一轮循环harness 都会把之前的对话记录原样拼接进上下文没有做裁剪或压缩。系统没崩溃纯粹是最后模型输入太长触发了超时而 harness 把超时当成了普通失败于是又发起新一轮重试形成一个死循环。6.4 第三步核对输入输出确认根因找到循环以后我本来以为问题就出在上下文膨胀。但当我对比正常 trace 和异常 trace 的输入输出时又发现了更底层的原因。正常 trace 里validate_document返回的结果包含 3 个字段状态、内容、修改建议。异常 trace 里返回结果只有 2 个字段那个代表“是否通过”的字段不见了。因为字段缺失模型在后续判断时永远无法知道“校验已经通过”所以只能一遍遍重新发起校验。为什么字段会不见查了代码之后发现是在一次工具返回结构升级时新结构的字段从approved改成了is_valid但 harness 里的映射逻辑没同步更新系统在组装上下文时把is_valid字段值原样传给了模型而模型并不知道这个新字段名的含义于是每次都在“试图寻找approved”的路上反复徘徊。这个根因如果只看代码可能要看半天但有了 trace 的输入输出对比几分钟就能锁定。6.5 修复与回归修复分两步。第一步先修 harness 的字段映射把新字段名正确映射到模型预期的语义上这是根因修复。第二步调整上下文压缩策略当历史轮次超过指定阈值时把中间过程记录压缩成摘要而不是原样拼接避免上下文膨胀再次触发超时。修复之后我没有直接上线而是先把当时存下来的异常 trace 样本转成了 dataset然后跑了一轮回归确认同样的输入已经能正常走完链路。之后又在灰度环境跑了一天观察 trace 里的循环调用次数是否回到正常区间。整个过程验证下来平均追踪耗时从 12 分钟降到了 40 秒彻底解决了用户“聊到最后没结果”的投诉。这次排障给我的最大感受是没有 trace我只能看到一个抽象的“Agent 很慢”有了 trace我才能看到每一次循环里的上下文、每一个工具返回的字段、每一次模型决策的输入依据。7. 别让追踪本身变成新的黑箱工程化的几个经验7.1 埋点粒度默认“对齐可交付物”埋点不是越细越好。把每一行代码都包一层装饰器看起来数据很丰富但实际排查时根本分不清重点。我自己的原则是埋点粒度对齐 harness 的“可交付物”也就是那些会影响最终结果的环节。需要埋的点只有四类一次完整任务顶层入口、一次模型调用、一次工具执行、一次关键决策比如分支、重试、回退。至于内部的字符串拼接、格式化、日志输出除非是性能瓶颈否则没必要单独埋。埋点过细会导致两个问题一是数据量太大查询和存储成本变高二是负责人会被大量无意义 trace 淹没反而找不到关键路径。7.2 采样策略与成本控制生产环境的 Agent 调用量通常不小全量埋点在成本上不一定划算。我实践的方案是默认对生产环境按 10% 到 20% 的比例采样异常时临时提升采样率对测试和回归环境则全量埋点。采样看起来简单但有前提你必须有办法在异常发生时提升采样率。我一般会在 harness 里加一个动态采样开关用配置中心或者环境变量控制。当发现某段时间的 trace 指标异常或者用户投诉增多时立刻把采样率提到 100%等问题定位之后再降回来。7.3 数据脱敏运行时信息不进 trace追踪数据里最容易出问题的就是敏感信息。用户的姓名、手机号、身份证号、内部文档内容这些都不应该原样写进 trace。好在 LangSmith 允许你在埋点时对 inputs/outputs 做过滤处理。我的做法是在接入层做一个统一的脱敏函数对每个要写入 trace 的字段先检查是不是敏感字段如果是就替换成掩码或者只保留结构信息比如把手机号变成138****1234把文档摘要变成“用户已授权共 5 段内容”。另外对于用户上传的原始文件内容我会在 trace 里只记录文件索引和大小不记录文件正文。这里特别提醒一下不要只在 UI 层做脱敏因为 trace 数据会落库、会被按条件筛选、会被导出成 dataset。必须在写入时就处理干净否则后续的数据链路里隐患非常大。7.4 命名、标签、团队规范最后一点是工程规范。LangSmith 的查询功能很强大但它依赖一个前提你的 Run 名称和标签是统一的。否则“按工具名筛选”这种操作就会因为命名不一致而漏数据。我通常会定一套命名规范顶层入口统一以agent_开头比如agent_ecommerce_booking。模型调用统一以model_开头带上模型标识和用途比如model_glm4_extract。工具调用统一以tool_开头带上领域比如tool_store_query、tool_payment_create。关键决策点统一以control_开头比如control_retry_guard、control_fallback_switch。这套规范看起来琐碎但当你同时维护五六个 Agent、每个 Agent 又有十几个工具时就会发现统一的命名规则是检索效率的关键。配合 metadata 里的环境标签、版本标签、实验组标签基本能实现“按任何维度快速拉出一批可对比 trace”的能力。最后再分享一个我自己的心得接入 LangSmith 的过程其实是一次 harness 的规范化梳理。当你开始琢磨每一个环节要怎么命名、怎么分类、怎么记录输入输出时你自然会发现代码里的隐藏分支、隐式回退和结构漂移问题。这也是我把可观测性放在工程实践里非常靠前位置的原因——它不只是拿来“看”的工具更是一面能照出 harness 设计质量的镜子。