给受控 Agent 接入 LangSmith:如何观测 HITL / Resume 生命周期

本文基于 AI Mind 项目的真实实现整理。

GitHub:github.com/HWYD/ai-min...

对应代码版本:v0.3.4

线上链接:ai.hwyblog.cloud/instant-min...

AI Mind 是一个持续迭代中的 Next.js AI Chat 项目。从最基础的本地聊天开始,逐步加入流式协议、工具调用、MCP、Skill 和 Agent 能力。

如果你对这个项目感兴趣,或者这篇文章对你有一点帮助,也欢迎顺手到 GitHub 帮 AI Mind 点个 Star⭐,这会是对我继续更新很大的鼓励。

在 v0.3.0 版本里,AI Mind 的 Tasklist Agent 已经支持了 HITL、PostgreSQL checkpoint 和 resume。

也就是说,它可以在关键节点暂停,等待用户审核,然后从同一个 Graph 执行线程继续往下跑。

这条链路跑通之后,新的问题也随之出现了:

text 复制代码
这轮 Agent 是从哪个 run 开始的?
它暂停在策略审核,还是修订审核?
用户选择了 approve、edit、reject,还是 respond?
恢复之后是 completed、blocked、rejected,还是 failed?
如果失败,失败发生在首次运行、暂停点持久化、恢复执行,还是最终结果分类?

这些问题不一定会影响用户当下使用,但会直接影响维护者排查问题的效率。

尤其是 HITL / Resume 这种链路,天然会跨请求、跨暂停点、跨 checkpoint。如果只看日志、前端展示和数据库状态,最后往往需要人工拼上下文。

v0.3.4 做的就是给这条链路补一层可观测性:

用 LangSmith 记录 Tasklist Agent 在 HITL / Resume 过程中的关键生命周期。

这篇文章不会写成 LangSmith SDK 接入教程。相比"怎么调用 SDK",我更想复盘的是:在一个已经有 HITL、checkpoint、业务状态和流式输出的受控 Agent 里,怎样接入可观测性,同时不破坏原有边界。

v0.3.4 的约束很明确:

text 复制代码
不修改 Graph 结构
不修改 HITL 决策协议
不修改流式协议
不修改 Prisma schema
不修改 checkpoint schema
不上传完整提示词
不上传完整 tasklist markdown
不把 LangSmith 当成业务事实源
不让观测失败影响主流程

这也是这一版最值得记录的地方。


1. 为什么 HITL / Resume 之后更需要可观测性

没有 HITL 的 Agent 流程通常比较直:

text 复制代码
用户请求
  -> Agent 执行
  -> 输出结果
  -> 请求结束

加入 HITL 和 Resume 后,执行链路会变长:

text 复制代码
首次运行
  -> 策略审核暂停
  -> 用户决策
  -> 恢复执行
  -> 可能进入修订审核暂停
  -> 再次用户决策
  -> 再次恢复执行
  -> final / blocked / rejected / failed

这已经不是一个请求里能完整描述清楚的过程。

策略审核会先暂停,用户提交审核决策后,再通过恢复请求继续执行。 如果后面触发修订审核,还会再次进入一个受控暂停点。

当线上出现问题时,维护者需要快速知道:

text 复制代码
这轮 runId 是什么?
threadId 是什么?
assistantMessageId 是什么?
经历了哪些暂停点?
用户提交了哪种审核决策?
最后结果是什么?
失败信息是否已经脱敏?

这些不是业务输出的一部分,但对排查很关键。

所以 v0.3.4 的目标不是继续增强 Agent 能力,而是让 Tasklist Agent 的关键生命周期可以被定位、串联和复盘。


2. v0.3.4 的目标:只观测 Tasklist Agent,不做全平台观测

这一版的范围很收敛。

LangSmith 只接入这条路径:

text 复制代码
/tasklist + @docs://versions/*.md

也就是 Tasklist Agent。

普通聊天、reader-skill、utility-skill、MCP、普通工具调用,都不在本版本范围内。

这样做是为了避免范围失控。

如果一开始就把 LangSmith 接成全平台观测,很容易引出一串新问题:

text 复制代码
普通聊天要不要记录?
reader-skill 的资源内容要不要上传?
MCP resource 怎么脱敏?
工具调用参数和结果要不要记录?
多模型调用要不要采样?
要不要做 Dataset?
要不要做 Evaluation?
要不要做内置 Trace Viewer?
要不要做 Run History?

这些问题不是不能做,而是不应该在 v0.3.4 里一起做。

这一版只解决一个问题:

让 v0.3.0 已经落地的 Tasklist Agent HITL / Resume 链路具备可排查的生命周期观测。

所以它不做:

text 复制代码
不做 LangSmith Dataset
不做 Evaluation
不做 Experiment Compare
不新增内置 Trace Viewer
不新增 Run History
不新增 agent_run_events
不做 pending HITL 刷新恢复
不做完整聊天消息持久化

对 AI Mind 这种持续迭代项目来说,先把一条复杂链路观测清楚,比一次性铺开所有路径更稳。


3. 可观测性不等于全量上传提示词

接入 LangSmith 时,一个很自然的想法是:直接打开自动追踪,把模型调用、提示词、输入输出都记录下来。

这样排查模型行为确实方便。

但 v0.3.4 没有把它作为默认方案。

原因很简单:Tasklist Agent 里有不少内容不适合默认上传到外部观测平台。

比如:

text 复制代码
完整提示词
完整 version plan 正文
完整用户反馈
完整 tasklist markdown
完整 GraphState
raw checkpoint
raw provider error
raw Prisma error
API Key
session cookie
ownerSessionHash
provider config

这些数据里,有些是内部运行状态,有些是用户输入,有些是系统提示词,有些是业务产物正文。

如果为了排查方便,把它们默认上传到外部服务,短期调试会更顺手,但长期会让隐私边界和数据边界变得模糊。

所以 v0.3.4 选择的是"元数据优先"。

也就是:只显式上传经过白名单筛选的生命周期元数据,不默认上传完整提示词、模型输入输出和业务正文。

允许上传的主要是定位和摘要字段:

text 复制代码
runId
threadId
assistantMessageId
interruptId
versionPlanUri
modelId
provider
reasoningEnabled
interruptKind
decisionType
reviewRound
fixNowCount
blockingIssueCount
weakSectionCount
durationMs
failureCode
sanitizedFailureMessage

这些字段足够回答"这轮 Agent 发生了什么",但不会把完整业务上下文暴露出去。

这里的取舍可以简单概括为:

text 复制代码
自动全量追踪更方便。
白名单元数据更可控。

在当前阶段,我更倾向先选择可控。

后续如果要做更深层的模型调用追踪,可以单独评估脱敏、采样策略和用户可见说明,再决定是否扩大上传范围。


4. 观测点放在哪里:协调器 / 运行器,而不是审核节点

v0.3.0 做 HITL 时已经确定过一个原则:

审核节点必须保持无副作用。

原因是 LangGraph 的 interrupt 在 resume 后,会重新执行触发 interrupt 的节点。

如果审核节点在 interrupt 前调用模型、写数据库、输出产物,就可能在恢复执行时重复触发。

到了 v0.3.4,这个原则继续保留。

LangSmith 观测逻辑不写进:

text 复制代码
review_tasklist_strategy
review_tasklist_revision

也就是说,审核节点不调用 LangSmith。

观测逻辑放在协调器和运行器边界。

大致结构是:

text 复制代码
chat-orchestrator
  -> startVersionPlanTasklistAgentRun
       -> 创建 AgentRun
       -> 记录首次运行
       -> 执行 Graph
       -> persistGraphResult
            -> interrupted: 持久化 AgentInterrupt + 记录暂停点
            -> completed: 标记 completed + 记录结果
            -> rejected: 标记 rejected + 记录结果
            -> failed: 标记 failed + 记录结果

resume API
  -> resumeVersionPlanTasklistAgentRun
       -> beginResume
       -> 记录用户决策 / 恢复执行
       -> 执行 Graph resume
       -> persistGraphResult
            -> interrupted / completed / rejected / failed

这样做有几个好处。

第一,审核节点仍然保持纯粹,不因为接入观测而变复杂。

第二,协调器本来就掌握 AgentRun / AgentInterrupt 的业务状态,更适合记录生命周期。

第三,LangSmith 失败可以在观测适配层里统一处理,不会污染 Graph 节点。

第四,后续如果要替换观测实现,也不需要改 Graph 拓扑和 HITL 决策协议。

这条边界很关键:

观测逻辑可以靠近运行时边界,但不要进入需要保持纯粹的 Graph 节点。


5. 一轮 Tasklist Agent 运行如何关联起来

HITL / Resume 最大的问题之一,是它不是一次请求完成的。

所以观测系统里必须有一组稳定的关联字段。

v0.3.4 选择了三个核心 ID:

text 复制代码
runId
threadId
assistantMessageId

它们分别对应不同视角。

runId:业务运行视角

runId 表示这一轮 AgentRun。

后端判断运行状态、待处理审核点、最终结果时,主要围绕它展开。

它能回答:

text 复制代码
这是哪一轮 Tasklist Agent 运行?
当前状态是 paused、resuming、completed、rejected,还是 failed?

threadId:Graph 恢复视角

threadId 对应 LangGraph checkpoint resume 的执行线程。

它能回答:

text 复制代码
Graph 应该从哪个 checkpoint 继续?
首次运行和恢复执行是否还在同一条线程里?

assistantMessageId:前端消息视角

assistantMessageId 对应前端那条助手消息。

它能回答:

text 复制代码
恢复后应该更新哪一条消息?
这次 Agent 执行轨迹应该归属到哪条助手消息?

这三个 ID 放在 LangSmith 元数据中,就能把首次运行、暂停点、用户决策、恢复执行和最终结果串起来。

HITL 相关事件还会带上这些摘要字段:

text 复制代码
interruptId
interruptKind
decisionType
reviewRound
draftRevision
fixNowCount
blockingIssueCount
weakSectionCount

这些字段不还原完整上下文,但足够判断这轮 Agent 走了哪条分支。

例如:

text 复制代码
interruptKind = strategy_review
decisionType = approve
reviewRound = 1

可以看出用户在第一次策略审核时选择了继续。

再比如:

text 复制代码
interruptKind = tasklist_revision_review
fixNowCount = 2
blockingIssueCount = 1
decisionType = respond

可以看出 tasklist 草稿进入了修订审核,并且用户选择了反馈修订。

这就是"元数据优先"的价值:不用上传完整正文,也能复盘生命周期。


6. tags 与 metadata 怎么分层

接入观测系统时,还有一个容易忽略的问题:哪些字段放 tags,哪些字段放 metadata?

这里我保留两个英文词,因为它们是 LangSmith 里的概念。

v0.3.4 的规则是:

tags 放低基数字段,metadata 放高基数字段。

低基数字段适合做 tags,比如:

text 复制代码
ai-mind
tasklist-agent
hitl
initial
resume
completed
blocked
rejected
failed
development
production
demo

这些字段种类少,适合在 LangSmith 里做粗粒度过滤。

高基数字段不适合放 tags,比如:

text 复制代码
runId
threadId
assistantMessageId
interruptId
versionPlanUri

因为这些字段每次运行几乎都不同。如果都放进 tags,后面检索和聚合会越来越乱。

所以它们更适合放 metadata。

可以简单理解成:

text 复制代码
tags 用来筛选一类运行。
metadata 用来定位某一次运行。

这个细节看起来不大,但长期看很有价值。

如果一开始不控制 tags,很容易把观测系统变成一堆随机 ID。后面想按状态、环境、路径做筛选时,反而不好用。


7. LangSmith 失败必须与主流程隔离

LangSmith 是观测层,不是主链路。

所以 v0.3.4 有一个硬要求:

LangSmith 失败不能影响 Tasklist Agent。

下面这些情况都必须被隔离:

text 复制代码
LangSmith client 初始化失败
trace 创建失败
event update 失败
flush / end 失败
LANGSMITH_TRACING=true 但缺少 API key
API key 配错
外部服务临时不可用

这些问题最多输出脱敏 warning,不能影响主流程:

text 复制代码
不能改变 AgentRun status
不能影响 AgentInterrupt pending / decided 状态
不能影响 LangGraph checkpoint resume
不能中断流式输出
不能影响 artifact 输出
不能吞掉业务失败

如果一个外部观测服务失败,就导致 Agent 主流程失败,那么可观测性反而成了新的故障点。

更合理的做法是:

text 复制代码
Agent 主流程优先。
观测尽力而为。
观测失败必须隔离。

所以 v0.3.4 增加了一个小型观测适配层,用来统一处理:

text 复制代码
配置解析
元数据白名单
tags 构造
LangSmith SDK 调用
错误捕获
失败隔离

这样主流程不需要到处处理 LangSmith 细节,也不会让外部服务错误泄漏到 Agent 运行状态里。


8. 为什么不把 LangSmith trace id 写进数据库

v0.3.4 没有新增数据库字段,也没有保存 LangSmith trace id。

这个决定一开始看起来有点保守。

毕竟如果把 trace id 存到 AgentRun 里,后续在页面上直接跳转 LangSmith,好像会更方便。

但它会引入新的持久化契约:

text 复制代码
需要修改 Prisma schema
需要新增 migration
AgentRun 会和外部观测平台产生持久化耦合
后续可能自然滑向 Run History / Trace Viewer
trace id 的生命周期也要被业务系统考虑

而 v0.3.4 的目标不是做内置 Run History,也不是做 Trace Viewer。

这一版只需要让维护者能在 LangSmith 中通过:

text 复制代码
runId
threadId
assistantMessageId

定位对应 trace。

这些 ID 已经足够做人工检索和关联。

所以 v0.3.4 保持了原有状态边界:

text 复制代码
AgentRun / AgentInterrupt:
  仍然是业务状态。

PostgresSaver checkpoint:
  仍然是 Graph 恢复状态。

LangSmith trace:
  只是外部观测状态。

LangSmith 不成为业务事实源。

一旦观测数据被业务逻辑依赖,后续可观测系统的启停、迁移、失败处理都会变得复杂。


9. 最小配置:使用官方 LangSmith env

配置上,v0.3.4 选择使用 LangSmith 官方环境变量:

env 复制代码
LANGSMITH_TRACING=false
LANGSMITH_API_KEY=
LANGSMITH_PROJECT=ai-mind-dev

没有再新增类似这样的二次开关:

env 复制代码
AI_MIND_LANGSMITH_ENABLED=

原因很直接:双开关会增加排查成本。

如果官方 LANGSMITH_TRACING=true,但项目自己的 AI_MIND_LANGSMITH_ENABLED=false,后面排查时就要先确认到底哪个开关生效。

所以启用判断保持简单:

text 复制代码
LANGSMITH_TRACING === "true"
并且 LANGSMITH_API_KEY 存在

如果没有 API key,就视为未启用。Tasklist Agent 正常运行,只是不创建 LangSmith trace。

LANGSMITH_ENDPOINT 也没有作为主配置放进环境变量模板。自托管 LangSmith 或特殊 endpoint 场景,可以在文档中说明,但不作为 MVP 默认配置。

常规场景只保留最小配置,特殊场景放到文档里。


10. 这版刻意不做什么

v0.3.4 的目标很明确:给 Tasklist Agent 的 HITL / Resume 生命周期补上可观测性。

所以它刻意不做很多事。

这一版不做:

text 复制代码
不做全平台 tracing
不做 LangSmith Dataset
不做 Evaluation
不做 Experiment Compare
不新增 AI Mind 内置 Trace Viewer
不新增 Run History
不新增 agent_run_events
不保存 LangSmith trace id 到数据库
不做 pending HITL 刷新恢复
不做完整聊天消息持久化

同时,它也不修改:

text 复制代码
GraphState shape
Graph topology
HITL decision schema
stream chunk schema
frontend reducer
Prisma schema
PostgresSaver checkpoint schema

这些边界保证了 v0.3.4 仍然是一个跨边界运行时集成版本,而不是架构事实源变更版本。

它涉及协调器、运行器、环境配置、元数据、脱敏、文档和测试,但没有改变 Tasklist Agent 的业务执行逻辑。

这也是这一版的价值:

增加可观测性,但不顺手改主链路。


11. 总结:Agent 可观测性首先是边界设计

做完 v0.3.4 后,我对 Agent 可观测性的理解更明确了一些。

它不是简单接入 LangSmith。

至少在 AI Mind 这个阶段,我更关心这些问题:

text 复制代码
观测哪条 Agent 路径?
观测哪些生命周期事件?
哪些字段允许上传?
哪些字段必须禁止上传?
观测逻辑应该放在哪一层?
观测失败是否会影响主流程?
观测数据是否会变成业务事实源?

v0.3.4 的答案是:

text 复制代码
只观测 Tasklist Agent
只记录 HITL / Resume 生命周期元数据
只上传显式白名单字段
观测点放在协调器 / 运行器
审核节点继续保持无副作用
LangSmith 失败必须与主流程隔离
LangSmith 不进入 GraphState / DB / stream / frontend reducer

所以这篇文章最后想表达的是:

给 Agent 接入可观测性,不是把所有内容都上传到外部平台,而是在不破坏运行时边界和隐私边界的前提下,记录足够排查问题的生命周期线索。

v0.3.4 不是一个很显眼的功能版本。 它不会让用户看到新的 Agent 能力,也不会改变原来的 Tasklist Agent 流程。

但从工程角度看,它补上了一个长期会越来越重要的能力:

让复杂 Agent 链路变得可定位、可复盘、可维护。


项目地址

👉 GitHub:github.com/HWYD/ai-min...

👉 线上体验:ai.hwyblog.cloud/instant-min...

如果这篇文章或者 AI Mind 项目对你有所帮助,也欢迎顺手帮项目点个 Star⭐。这个支持对我来说很重要,也会让我更有动力继续整理后续版本的实现过程、设计取舍和踩坑复盘。

相关推荐
大流星3 小时前
LangChainJs之Prompt提示词(二)
javascript·langchain
满怀冰雪3 小时前
24_中间件系统源码分析_Middleware链的洋葱模型与异常处理
人工智能·python·中间件·langchain
如何原谅奋力过但无声4 小时前
LangGraph 中如何实现状态注入:InjectedState
langchain
行者-全栈开发4 小时前
【Spring AI 】AI Agent 架构设计:感知、规划、行动、记忆四层模型深度解析(2026最新)
langchain·autogen·spring ai·企业级应用·智能体设计·agent架构·四层模型深度解析
齐 飞20 小时前
LangChain快速入门-03Retrieval(上)
langchain
怕浪猫21 小时前
第9章 实战项目一:智能数据分析Agent
langchain·aigc·ai编程
invicinble1 天前
总结一下LangChain+LangGraph这套体系
langchain
糖果店的幽灵1 天前
langchain 所有内置中间件最详细解析
中间件·langchain
阿拉斯攀登1 天前
Agent 框架对比:LangChain / AutoGPT / CrewAI
人工智能·langchain·agent·rag·function