LangSmith中graph run与assistant run的区别及trace定位方法

发布时间:2026/10/16 19:13:50
LangSmith中graph run与assistant run的区别及trace定位方法 做 agent 调试的时候我一开始总被 LangSmith 里一个个 run 的名称绕晕同一个 Thread 下面挂了好几条 Trace每条 Trace 里面又嵌套着 LangGraph、assistant、ChatOpenAI、Tool 各种 run。而最让我头痛的问题只有一个——到底哪个是 graph run哪个是 assistant run这两个东西之间又是什么关系。一开始我以为它们是两个完全独立的东西后来把 trace、thread、graph、assistant run 放进同一个坐标体系里去理解结论才真正清晰。这篇内容就是我平时在 LangSmith 里区分这些概念的完整方法看完你至少能少走一半弯路。1. trace、thread、run先把坐标体系搭起来1.1 trace一次执行就是一条完整链路链路要正确理解最好先看 trace 的粒度。你调用一次 agentLangSmith 就会把这次请求从进来到出去的完整过程记录下来包括中间所有 LLM 调用、工具调用、状态更新最后串成一颗执行树。这棵树就是一条 trace。这颗树上每个节点都是一个 run。比如一个很常见的客服 agenttrace 的大致形态会是这样trace根 runagent/graph 的入口 ├── ChatOpenAIllm run ├── 工具调用tool run │ └── 工具内部再调一次模型llm run └── ChatOpenAIllm run输出最终结果第一次看 trace 的时候很容易误以为“一条 trace 就是一条 run”其实不对。trace 是“一次完整请求”run 是“这次请求里的一个环节”。一个 run 下面还可以挂子 run整体是一棵树而不是一根链条。这个粒度问题特别重要。因为你会发现如果你连续调用两次同一个 agentLangSmith 里会出现两条 trace而且两条 trace 的结构几乎一模一样。这不是数据重复而是两次独立请求各自生成了一条执行树。只要你还沉浸在“一次 trace 等于一次对话”的错觉里后面一定会把 thread 的概念搞混。1.2 thread跨 trace 的会话维度thread 的粒度比 trace 高一层它才是“会话”的载体。在很多状态化 agent 架构里比如用 LangGraph 做 checkpoint 持久化的时候同一个会话会有多个用户轮次。用户每发一句话agent 就执行一次产生一条 trace。但这一整段连续对话是同一个会话需要用同一个 thread_id 把它们串起来。LangSmith 在做展示时就会把具有相同 thread_id 的一批 trace 聚合到同一个 thread 下面。也就是说thread 是“会话”trace 是“这个会话里的某一次执行”run 是“这一次执行里面的某个子步骤”。用一个更容易理解的类比thread 像一个小区的楼栋号trace 像某一户的一次上门维修run 则是维修工走进门后做的每一个具体动作。你可以在同一次维修里做很多动作也可以在同一个楼栋里发生很多次维修。彼此独立但又有归属关系。在 LangSmith 的 Threads 相关页面里你可以按 thread_id 把 trace 分组。点开一个 thread看到的往往是多次用户轮次产生的多条 trace而不是一条大长长的 trace。这个展示方式本身就暗示了 thread 和 trace 之间是“一对多”的关系。1.3 run一切追踪的最小计量单位run 是 LangSmith 里最底层的基本单元。一次 LLM 调用是一个 run一次工具执行是一个 run一个 Runnable 中间节点也是一个 run。LangSmith 会给 run 打上类型标签常见的大类有llm大模型生成调用chain组合逻辑、Agent、Graph 等运行逻辑tool工具函数执行retriever检索器调用embedding向量化调用prompt、parser提示词拼装和输出解析。区分 graph run 和 assistant run 时最应该关注的就是llm、chain、tool这三种类型。单纯看名字不一定可靠因为开发者可以自定义 run 名称但只要你把 run_type、名称、层级三个信息结合起来基本不会认错。再补一个关键认识run 之间不只有兄弟关系还有父子关系。父子关系的产生方式取决于代码是怎么包的。比如一个自定义 Agent 里调用了模型这个 Agent 逻辑就会生成一个父 run模型调用生成子 run。LangGraph 的节点执行也一样节点本身是一个 run节点里面再调 LLM 或工具时LLM 和工具又作为子 run 出现。后面的 graph run 识别主要靠的就是这套层级。2. 在 trace 里识别 graph run2.1 graph run 的外貌结构比名称更重要当你在 LangSmith 里看到一颗执行树怎么判断它的根是不是 graph run我的习惯是三步走。第一步看名称第二步看 run_type第三步看子树结构。名称上使用 LangGraph 的程序根 run 默认经常会显示成LangGraph或者你给 graph 起的名字。这个只能作为参考因为有人会把外层套一层自定义 Runnable最后顶层 run 就变成了自定义名称。真正比较稳的判断是 run_typegraph 入口本身属于chain类型它不是一次 LLM 调用也不是一次工具调用。但chain类型还不能把 graph 和普通 Agent 区别开所以最关键的还是子树结构。LangGraph 的执行树里通常会出现以图节点命名的子运行比如assistant、tools、router、__start__这些。一个典型的 LangGraph trace 会长得像下面这样LangGraphchain总入口 ├── __start__chain入口节点 ├── assistantchain处理节点 │ ├── ChatOpenAIllm │ └── 结构化输出parser ├── toolschain工具分发节点 │ └── search_webtool └── 收尾节点chain虽然具体节点名会因为你的图设计而不同但“节点包着一层内部调用”的结构在 LangGraph 里非常明显。如果你看到某个 root run 下面出现了一层名称像节点名的子 run而这些子 run 下面又各自挂着 LLM 工具调用那它基本就是 graph run。2.2 同一 thread 下多个 graph run 怎么区分很多人在 Thread 页面里看到一个 thread 下面挂着好几条 trace每条 trace 的 root 都叫LangGraph或者assistant就会开始怀疑是不是 LangSmith 把同一个 run 重复记录了。其实不是。同一个 thread 下的多条 trace通常是同一个 graph 在不同用户轮次里的执行结果。每条 trace 都代表一次独立的 graph 调用只是这些调用共享同一份会话状态。判断方法很简单对比两条 trace 的start_time。如果时间不同、输入信息不同那这就是一个 thread 下的多次 graph run。如果再仔细看你会发现第一条 trace 的输出变成了第二条 trace 的输入状态的一部分因为会话状态在 checkpoint 里被保存并传递了。这也是区分“一条 trace 里多个 run”和“一个 thread 下多个 trace”的核心场景。前者是一棵树内部的父子关系后者是同一个会话下多棵树之间的兄弟关系。2.3 普通 chain run 与 graph run 的混淆点没有用 LangGraph 的普通 Agent比如老的 AgentExecutor 链路也会生成长得很像的树root run LLM 子 run 工具子 run。那怎么区分我一般看两点一是有没有带节点语义的中间层级。普通 AgentExecutor 通常更“平铺”一些root 下面直接挂着 LLM 调用和工具调用不会像 LangGraph 那样先出现一个节点层再在节点层下面出现具体调用层。二是在 trace 详情里查看 metadata。LangGraph 如果启用了 checkpoint 持久化trace 里一般会出现与 checkpoint 相关的标识比如checkpoint_id、thread_id这样的字段这些字段在普通 chain run 里很少出现。如果你在 metadata 里看到thread_id基本可以断定这是 stateful 的 graph run。这个判断不依赖名称所以即使你把图名称改成了my_chat_agent也照样能认出来。3. assistant run 到底在 trace 里长什么样3.1 “assistant” 这个词有两种使用场景assistant run 之所以容易和 graph run 混淆是因为“assistant”这个名字在不同语境下指的东西不一样。第一种场景是 OpenAI 风格的助手体系。在那里有Assistant、Thread、Run三个对象。Assistant是助手配置Thread是会话Run是某次执行。如果你在自己的 agent 里套用了这套 API那 LangSmith 里会出现跟 OpenAI Run 对象对应的 trace 信息比如run_id、assistant_id、thread_id。很多人在这个场景下问“assistant run 和 graph run 有什么区别”其实是在问“OpenAI 的执行对象和 LangGraph 的执行对象怎么对应”。第二种场景是 LangGraph 平台的部署形态。在 LangGraph 的部署体系里一个配置好的 graph 可以称为一个 assistant客户端以“assistant”为入口发起调用服务端再触发一次图执行。这种情况下从业务视角看它是“一次 assistant run”从底层图视角看它仍然是“一次 graph run”。这两个视角从不同侧面描述了同一个事实。真正在 LangSmith 里 trace 不会叫自己“assistant run”它只会记录具体发生了哪些 run。所以你不应该搜索一个叫assistant run的实体而应该通过 trace 里的名称、metadata 和层级去反推这次执行对应的是哪个 assistant 配置。3.2 识别 assistant 的两个信号assistant_id 与 thread_id不管 assistant 是哪种场景识别它的最强信号都是assistant_id。如果你在 trace 的输入参数或 metadata 里看到assistant_id那这次执行大概率是一次 assistant 维度的调用。如果再配合thread_id那就能确定它是在某个会话线程上的一次 assistant 执行。举个我实际排查过的例子。一个部署在服务端的 agent客户端每次发消息都会带上{ thread_id: customer-1001, assistant_id: asst_customer_service }服务端接收到这个参数后调用 graph。LangSmith 里对应的 trace 就会出现这样的 metadatametadata: thread_id: customer-1001 assistant_id: asst_customer_service但只看 trace 列表的话root run 的显示名称可能仍然是LangGraph或自定义名称。如果你只盯着名称很难判断这是不是 assistant run。可一旦你把assistant_id和thread_id这两个字段拿出来一条 trace 到底来自哪个助手、哪个会话就一目了然了。3.3 graph run 与 assistant run 的区别一个是主干一个是入口视角把这两个概念放在一起看我的理解是graph run 是“图执行”这个技术事实assistant run 是“业务入口”这个调用视角。如果直接调用graph.invoke(...)LangSmith 里体现的就是 graph run不会有 assistant 的概念参与其中。如果通过一个 assistant 配置发起调用那这次调用在最上层会带 assistant 相关信息但落到执行层面它仍然是一个 graph 的完整执行。用一句话概括不是你多了一个“assistant run”要处理而是同一个 graph 执行被套上了一个更上层的业务身份。理解为同一棵树的树根标识不同比理解为两个并列实体更准确。我把它们的区分维度整理成一张表区分维度graph runassistant run本质一次完整图执行一次带 assistant 配置的图执行入口识别依据树结构、节点层、checkpoint metadataassistant_id、assistant 名称、部署配置LangSmith 中的体现root run 及整棵子树trace 的 metadata、入口参数、有时会出现在 root 名称相互位置可以作为 assistant run 的执行主体通常会包住或指向一个 graph run 的执行是否必须有 thread不是必须可无状态执行通常配合 thread 使用尤其是有持久化会话时这张表回答的问题其实就是标题里最让人困惑的部分graph 和 assistant 不是两个平行 rungraph 是引擎assistant 是这趟引擎的一次命名航班。4. LangSmith UI 实操一套四步判定流程4.1 先进入 Threads 再找 Trace如果你已经知道自己要看某个会话不要从 Traces 页面里大海捞针。先在 LangSmith 的 Thread 视图里找到对应的 thread再点进去看这个 thread 下的 trace 列表这样能直接过滤掉无关干扰项。在 Thread 视图里你会看到一条一条的 trace相当于这个会话每一次用户请求产生的执行记录。通常展示的信息包括开始时间、状态、耗时、token 用量等。此时不要着急点开第一条而是先观察一下这个 thread 下有多少条 trace每一条 trace 的开头时间是否连续。这个动作解决的是“这个 thread 下是否有多轮 graph run”的基础判断。如果你发现有很多 trace但每个 trace 结构都差不多那就说明会话里发生过多次独立的 graph 执行而不是一次执行卡住了。4.2 定位 root run点击某一条 trace 进入详情页后不要从上往下乱拖先直接找 root run。root run 是整棵执行树的根节点也就是整条 trace 的最顶层入口。你需要在详情面板里确认 root run 的四个信息名称显示的是什么run_type是 chain、llm 还是 tool输入里面有没有 thread_id、assistant_id、messages 等业务字段metadataLangSmith 是否额外记录了你写入的自定义 metadata。这四个信息基本能确定一次执行的“身份”。比如 root run 名称是LangGraphrun_type 是chain但输入里带了assistant_id那它虽然看起来是 graph run但业务上是 assistant run。如果你在代码里只是裸调一个 LLM没有包 graphroot run 的 run_type 很可能直接是llm那这就是一次纯模型调用跟 graph 和 assistant 都没关系。4.3 顺着子树判断内部结构root run 判断完之后接着看它的 child run。把 tree 视图展开观察 root 下面第一层子 run。如果第一层子 run 是类似assistant、tools、retriever、__start__这样的节点名那说明整棵树的执行主体是 LangGraph graph。这就是 graph run 的实体证据。再往下一层看这些节点下面应该还会挂着更具体的 run比如某个 LLM 调用的ChatOpenAI、某个工具执行的函数名。如果你看到的层级非常扁平root 下面直接就是ChatOpenAI和工具调用没有节点过渡层那它更可能是普通 chain 或者 agent executor而不是典型 LangGraph graph run。这一步是在所有名称都不可信的情况下用结构做兜底判断。4.4 用筛选辅助定位特定 run当 trace 特别大、子 run 特别多时手动翻树效率很低。我的习惯是直接用 LangSmith 的搜索或筛选能力常见做法有两种第一种是按 run_type 过滤。比如我只想看模型调用就筛选 type 为LLM的 run我只想看 tools就筛选 type 为tool的 run。这样能在复杂树里快速定位到具体的计算节点再回看它们属于哪个父节点就能判断它们在 graph 里的位置。第二种是按 metadata 搜索。如果你在代码里给某个关键步骤加了 metadata比如config { configurable: {thread_id: customer-1001}, metadata: { assistant_id: asst_customer_service, flow_type: graph, }, }那么在 LangSmith 的筛选框里直接搜assistant_id或flow_type就能把相关 trace 拎出来。这个方法在排错时特别好用尤其当同一个 thread 下混杂了多条不同来源的 trace 时metadata 是最可靠的筛选条件。5. 日常排查中遇到的高频问题5.1 为什么同名 run 出现好多次这是出现频率最高的问题。一个 trace 里出现好几个ChatOpenAI或者好几个search_tool不是 LangSmith 合并失败而是这个 graph 在本次执行中真的调用了多次模型或多次工具。LangGraph 的 agent 循环里模型可能会被调用两三次。每次调模型LangSmith 都会生成一个新的 llm run。如果你只看到名字相同就以为只有一次 run后续分析 token 成本和耗时就会出大偏差。我的经验是遇到同名 run先看它们的start_time和input。如果输入不一样那就是多次真实调用。如果输入完全一样、时间几乎相同那就要注意是不是重试机制导致的重复执行。重试场景下同一个调用会产生多条 run 记录但输入往往一致这时候排查重点应该放在重试策略上而不是再纠结命名。5.2 trace 里明明有执行记录却看不到 thread_id有时候你确实用了 LangGraph也传了thread_id但 trace 列表里就是没体现同一个 thread 的分组。这种问题的原因通常不是 LangSmith 丢了数据而是这次调用没有真正走到带 checkpoint 的执行路径。最常见的错误姿势是创建 graph 时用了 InMemory 类型的持久化但每次请求都新建了同一个实例而 LangGraph 的持久化需要 graph 实例保持存在。或者你在invoke的时候没有把config传进去只在初始化时写了configurable那thread_id自然就传丢了。遇到这种情况我的排查顺序是先检查 graph 编译时是否注入了 checkpointer再检查调用时是否传了带thread_id的 config最后再看 LangSmith 里configurable字段是否真的出现在 trace 输入中。三步走完大部分 thread_id 消失的问题都能解决。5.3 root run 名称既不是 LangGraph 也不是 assistant很多同学习惯用名称判断一切看到 root run 叫my_wrapped_store就觉得这不是 graph run。其实名称是最不稳定的信号。开发者完全可以在代码里用with_config(run_nameanything)或者干脆在自定义 Runnable 的get_name()里返回任意字符串。LangSmith 只记录名称不校验名称。所以当名称无法帮你判断时回到我前面说的三层信号run_type、子树结构、metadata。只要这三样里有两样指向 graph那不管名称多奇怪它都是 graph 执行的一部分。名称能给你提供提示但不能作为唯一依据。5.4 assistant run 和 graph run 的耗时看起来差很多有一次我对比两个 trace一个 root 叫LangGraph耗时 20 秒另一个 root 叫assistant耗时只有 1 秒。我差点以为 assistant run 比 graph run 快其实完全不是这么回事。LangSmith 的耗时是 run 本身的耗时不是整个端到端业务耗时。如果存在外层 API 入口 run真正的排队时间、传输时间、业务处理时间都会体现在外层如果你只看内部某个 graph run它可能只计算了图实际执行的时间等待时间在外层已经消耗掉了。所以看到不同 run 的时长差异不要立刻归因到模型或工具性能。先确认两个 run 是不是同一层级。不同层级的耗时没有直接可比性父 run 的耗时永远包含子 run 的耗时而兄弟 run 的耗时才能直接对比。5.5 thread 和 trace 到底什么时候相等在无状态调用里一个 thread 只有一条 trace用户会觉得 thread 和 trace 是同一个东西。这种等价只在单一调用场景下成立一旦会话多轮化等价关系立刻消失。我的建议是在写代码和看 trace 时都严格区分业务维度和执行维度。业务维度是“这个用户在这个会话里问了几次”执行维度是“处理这一次提问时实际触发了几次内部执行”。thread 负责业务维度trace 负责执行维度run 负责具体步骤。把维度绑死后面很多看似复杂的问题都会简化成简单的计数题。6. 让 LangSmith 里的身份更清晰的几个习惯6.1 统一给 assistant 和 graph 写 metadata现在我在代码里有一个硬性习惯所有走 assistant 入口的调用都必须带上assistant_id和明确的flow_type。这样 LangSmith trace 的 metadata 面板里永远有业务身份可查。示例config { configurable: {thread_id: customer-1001}, metadata: { assistant_id: asst_customer_service, assistant_version: v1.2.0, flow_type: graph, }, } result app.invoke({messages: [{role: user, content: 查一下订单}]}, config)看起来只多了几行字段但在 LangSmith 里排查效率和没有 metadata 时完全是两个量级。当 trace 数量多起来以后搜索不比肉眼一个个看更香吗6.2 用 thread_id 约定会话却不混用身份我见过有人把thread_id直接当成业务用户 ID 来用这会导致同一个用户的多轮会话被压成同一个 thread排查历史时就很难区分是哪一轮先发生的。我自己的做法是thread_id一律采用“用户标识 会话编号”的组合比如customer-1001-session-20250412-01这样既保留了会话维度也能在 LangSmith 里一眼看出是哪一轮对话。虽然这个字符串会长一点但对比事后在几十条 trace 里翻半天这个成本非常划算。6.3 定期用一个已知场景做“标定”如果你经常被 trace 搞晕建议准备一个非常简单的场景专门用来标定你的 graph 结构和 assistant 信息。比如一个只有两三个节点的最小 graph每次都固定输入同一句话然后在 LangSmith 里观察它的 trace 长什么样。把这种标准 trace 截图保存下来以后遇到陌生的 trace拿来做结构对比就能很快发现哪些 run 是额外出现的哪些是预期内的。这类标定在 LangGraph 升级版本后尤其重要。因为 trace 展示方式可能随着版本更新而变化没有一份标准参考你很难判断到底是自己的代码变了还是展示变了。我用这套方法重新去读之前那些复杂 trace 时最直接的改变其实就一句话不再把名称当成身份而是把结构、层级、metadata 三者放在一起看。graph run 是引擎assistant run 是入口视角thread 是会话容器trace 是完整链路run 是具体步骤。把这一层关系理顺之后LangSmith 里再复杂的树也能在几分钟内拆完。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询