AI Agent(七):LangSmith 可观测性
·
目录
AI Agent 进阶:LangSmith 可观测性 —— 让 Agent 的行为"看得见"
🎯 从"黑盒运行"到"全链路透明"——用 LangSmith 追踪 Agent 的每一步决策
前情回顾:上篇文章的局限
在上一篇文章《LangGraph 工作流编排》中,我们实现了:
- ✅ 状态图编排 Agent 行为
- ✅ 条件边实现动态流程控制
- ✅ 多轮工具调用循环
- ✅ RAG 回退机制
但是,当 Agent 运行起来后,我们遇到了一个新问题:
问题:Agent 是一个"黑盒"
用户: "查询 CPH2223 的出货信息"
Agent 内部发生了什么?
┌─────────────────────────────────────────────────────────┐
│ ??? │
│ │
│ tool_decision → ??? → tool_execution → ??? → ??? │
│ │
│ LLM 做了什么决策? │
│ 工具调用了哪些参数? │
│ 每一步花了多长时间? │
│ 哪一步出了问题? │
│ │
│ 我们完全不知道! │
└─────────────────────────────────────────────────────────┘
真实场景中的痛点
| 问题 | 影响 | 严重程度 |
|---|---|---|
| Agent 给出错误答案 | 不知道是 LLM 决策错还是工具执行错 | 🔴 严重 |
| 响应特别慢 | 不知道是哪个环节慢 | 🟡 中等 |
| 工具调用失败 | 日志分散,难以串联完整链路 | 🔴 严重 |
| 多轮工具循环异常 | 不知道为什么 LLM 一直在循环 | 🔴 严重 |
简单来说:我们需要一个"上帝视角"来观察 Agent 的每一步行为。
什么是 LangSmith?
核心概念
LangSmith 是 LangChain 团队推出的 LLM 应用可观测性平台,专门用于追踪、调试和监控 LLM 应用。
┌─────────────────────────────────────────────────────────────┐
│ LangSmith 核心功能 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ Tracing(追踪) │
│ 记录每一次 LLM 调用、工具执行、检索操作 │
│ ┌──────────────────────────────────────┐ │
│ │ workflow_invoke │ │
│ │ ├── tool_decision_llm │ │
│ │ ├── get_shipment_info (tool) │ │
│ │ ├── tool_decision_llm │ │
│ │ └── final_response_llm │ │
│ └──────────────────────────────────────┘ │
│ │
│ 2️⃣ Debugging(调试) │
│ 查看每一步的输入输出、耗时、错误信息 │
│ │
│ 3️⃣ Monitoring(监控) │
│ 统计调用量、延迟、错误率、Token 消耗 │
│ │
│ 4️⃣ Evaluation(评估) │
│ 对比不同版本的 Agent 表现 │
│ │
└─────────────────────────────────────────────────────────────┘
类比理解
把 Agent 想象成一条流水线:
| 概念 | 没有 LangSmith | 有 LangSmith |
|---|---|---|
| 比喻 | 黑箱工厂 | 透明工厂 |
| 可见性 | 只看到最终产品 | 看到每一步加工过程 |
| 排错 | 拆开整个工厂 | 直接看监控摄像头 |
| 优化 | 盲目猜测 | 数据驱动 |
LangSmith = Agent 的监控摄像头 📹
为什么需要可观测性?
场景 1:Agent 给出错误答案
用户: "查询 CPH2223 的出货信息"
Agent: "该机型不存在" ← 但实际上机型是存在的!
没有追踪:
→ 不知道哪里出错了,只能加 log 重新运行
有追踪:
→ 打开 LangSmith 面板
→ 发现 tool_decision_llm 传给工具的参数是 "CPH222" 少了一个 "3"
→ 原来是 LLM 解析参数时出了问题
→ 修改 Prompt 即可
场景 2:响应特别慢
用户: "帮我排查一下出货信息"
Agent: (等了 30 秒才回复...)
没有追踪:
→ 不知道 30 秒花在了哪里
有追踪:
→ 打开 LangSmith 面板,看到:
→ workflow_invoke (30s 总耗时)
├── tool_decision_llm (2s) ← 正常
├── get_shipment_info (25s) ← 🔴 这里!API 超时
└── final_response_llm (3s) ← 正常
→ 定位到是出货信息 API 响应太慢
场景 3:工具循环调用
用户: "查询资源下发情况"
Agent: (工具被循环调用了 5 次才停下来...)
有追踪:
→ 在 LangSmith 中看到完整的调用树:
→ workflow_invoke
├── tool_decision_llm → 调用 get_rsc_list
├── get_rsc_list (tool) → 成功
├── tool_decision_llm → 又调用 get_rsc_list ← 为什么?
├── get_rsc_list (tool) → 成功
├── ...重复 5 次
→ 查看每次 LLM 的输入输出,发现是 Prompt 不够明确
→ LLM 不确定结果是否完整,所以一直在重复查询
我们的追踪架构设计
追踪层级
┌─────────────────────────────────────────────────────────────┐
│ 追踪层级设计 │
├─────────────────────────────────────────────────────────────┤
│ │
│ workflow_invoke (根节点 - Chain 类型) │
│ ├── inputs: { user_message, phase } │
│ ├── outputs: { response, tool_iterations } │
│ │ │
│ ├── tool_decision_llm (子节点 - LLM 类型) │
│ │ ├── inputs: { messages } │
│ │ └── outputs: { content, tool_calls_count } │
│ │ │
│ ├── get_shipment_info (子节点 - Tool 类型) │
│ │ ├── inputs: { tool_name, arguments, tool_id } │
│ │ └── outputs: { result } 或 { error } │
│ │ │
│ ├── tool_decision_llm (子节点 - LLM 类型) │
│ │ ├── inputs: { messages } │
│ │ └── outputs: { content, tool_calls_count: 0 } │
│ │ │
│ ├── rag_retrieval (子节点 - Retriever 类型) │
│ │ ├── inputs: { query } │
│ │ └── outputs: { context } │
│ │ │
│ └── final_response_llm (子节点 - LLM 类型) │
│ ├── inputs: { messages, tool_results } │
│ └── outputs: { response } │
│ │
└─────────────────────────────────────────────────────────────┘
Run 类型说明
LangSmith 定义了几种 Run 类型,对应不同的操作:
| Run 类型 | 说明 | 我们的使用场景 |
|---|---|---|
chain |
链式调用,通常是顶层 | 整个工作流 workflow_invoke |
llm |
LLM 调用 | 工具决策、最终回复生成 |
tool |
工具执行 | 各种业务工具调用 |
retriever |
检索操作 | RAG 知识库检索 |
代码实现详解
1. Tracer(跟踪器)—— 追踪的核心
// tracing/tracer.go
// Config LangSmith 配置
type Config struct {
APIKey string
ProjectName string
Enabled bool
}
// Tracer LangSmith 跟踪器
type Tracer struct {
client *langsmith.Client
projectName string
enabled bool
mu sync.Mutex
}
// NewTracer 创建新的跟踪器
func NewTracer(cfg Config) *Tracer {
if !cfg.Enabled || cfg.APIKey == "" {
log.Println("[Tracing] LangSmith tracing disabled")
return &Tracer{enabled: false}
}
client := langsmith.NewClient(
option.WithAPIKey(cfg.APIKey),
)
projectName := cfg.ProjectName
if projectName == "" {
projectName = "default"
}
log.Printf("[Tracing] LangSmith tracing enabled, project: %s", projectName)
return &Tracer{
client: client,
projectName: projectName,
enabled: true,
}
}
设计要点:
- 优雅降级:如果未配置 API Key 或未启用,Tracer 仍可创建,但所有操作都是空操作(no-op)
- 官方 SDK:使用
langsmith-go官方 Go SDK,保证协议兼容性 - 项目隔离:通过
ProjectName区分不同项目的追踪数据
2. Run(运行记录)—— 追踪的基本单元
// Run 表示一次运行跟踪
type Run struct {
tracer *Tracer
id string
traceID string
parentRunID string
dottedOrder string // LangSmith 必需的层级排序字段
name string
runType langsmith.RunRunType
inputs map[string]interface{}
outputs map[string]interface{}
startTime time.Time
endTime time.Time
error string
tags []string
extra map[string]interface{}
children []*Run
mu sync.Mutex
}
关键字段解释:
┌─────────────────────────────────────────────────────────────┐
│ Run 字段说明 │
├─────────────────────────────────────────────────────────────┤
│ │
│ id 唯一标识符(UUID) │
│ traceID 追踪链 ID(根节点的 id) │
│ parentRunID 父节点 ID(形成树形结构) │
│ dottedOrder 层级排序字段(LangSmith 特有) │
│ name 节点名称(如 "tool_decision_llm") │
│ runType 节点类型(chain/llm/tool/retriever) │
│ inputs 输入数据 │
│ outputs 输出数据 │
│ startTime 开始时间 │
│ endTime 结束时间 │
│ error 错误信息(如果有) │
│ children 子节点列表(形成树) │
│ │
└─────────────────────────────────────────────────────────────┘
3. dottedOrder —— LangSmith 的"黑魔法"
这是 LangSmith 独特的层级排序机制,必须正确实现,否则追踪数据无法正确展示:
// generateDottedOrder 生成 dotted_order 字符串
// 格式: {YYYYMMDDTHHMMSSffffffZ}{run_id}
func generateDottedOrder(t time.Time, runID string) string {
timestamp := t.Format("20060102T150405.000000Z")
// 移除点号,保持格式一致
timestamp = timestamp[:15] + timestamp[16:22] + "Z"
return timestamp + runID
}
层级关系通过 dottedOrder 表达:
根节点: 20260112T141347123456Zroot-uuid-1234
子节点1: 20260112T141347123456Zroot-uuid-1234.20260112T141348234567Zchild-uuid-5678
子节点2: 20260112T141347123456Zroot-uuid-1234.20260112T141349345678Zchild-uuid-9012
规则:
- 根节点:时间戳 + run_id
- 子节点:父的 dottedOrder + "." + 时间戳 + run_id
- 通过 "." 分隔表达层级关系
4. 父子节点关系 —— 构建追踪树
// StartRun 开始一个新的根运行
func (t *Tracer) StartRun(name string, runType langsmith.RunRunType,
inputs map[string]interface{}, tags []string) *Run {
if !t.enabled {
return &Run{tracer: t} // 空操作
}
runID := uuid.New().String()
startTime := time.Now().UTC()
dottedOrder := generateDottedOrder(startTime, runID)
return &Run{
tracer: t,
id: runID,
traceID: runID, // 🔑 根运行的 traceID 与 id 相同
dottedOrder: dottedOrder,
name: name,
runType: runType,
inputs: inputs,
startTime: startTime,
tags: tags,
extra: make(map[string]interface{}),
children: make([]*Run, 0),
}
}
// CreateChild 创建子运行
func (r *Run) CreateChild(name string, runType langsmith.RunRunType,
inputs map[string]interface{}) *Run {
if !r.tracer.enabled {
return &Run{tracer: r.tracer}
}
r.mu.Lock()
defer r.mu.Unlock()
childID := uuid.New().String()
startTime := time.Now().UTC()
// 🔑 子运行的 dotted_order = 父 + "." + 子
childDottedOrder := r.dottedOrder + "." + generateDottedOrder(startTime, childID)
child := &Run{
tracer: r.tracer,
id: childID,
traceID: r.traceID, // 继承根节点的 traceID
parentRunID: r.id, // 指向父节点
dottedOrder: childDottedOrder,
name: name,
runType: runType,
inputs: inputs,
startTime: startTime,
tags: r.tags, // 继承父节点的 tags
extra: make(map[string]interface{}),
children: make([]*Run, 0),
}
r.children = append(r.children, child)
return child
}
树形结构图解:
StartRun("workflow_invoke", Chain)
│
├── CreateChild("tool_decision_llm", LLM) ← parentRunID = root.id
│ traceID = root.id
│
├── CreateChild("get_shipment_info", Tool) ← parentRunID = root.id
│ traceID = root.id
│
└── CreateChild("final_response_llm", LLM) ← parentRunID = root.id
traceID = root.id
所有子节点共享同一个 traceID,形成一条完整的追踪链
5. 批量提交 —— 一次性上报所有追踪数据
// End 结束运行并提交到 LangSmith
func (r *Run) End(ctx context.Context) error {
if !r.tracer.enabled {
return nil
}
r.mu.Lock()
r.endTime = time.Now().UTC()
r.mu.Unlock()
// 递归收集所有需要提交的 runs(包括子节点)
runs := r.collectRuns()
if len(runs) == 0 {
return nil
}
// 🔑 关键:批量提交,减少网络请求
params := langsmith.RunIngestBatchParams{
Post: langsmith.F(runs),
}
_, err := r.tracer.client.Runs.IngestBatch(ctx, params)
if err != nil {
log.Printf("[Tracing] Failed to submit runs: %v", err)
return err
}
log.Printf("[Tracing] Successfully submitted %d runs", len(runs))
return nil
}
// collectRuns 递归收集所有运行记录
func (r *Run) collectRuns() []langsmith.RunParam {
if !r.tracer.enabled || r.id == "" {
return nil
}
r.mu.Lock()
defer r.mu.Unlock()
runs := make([]langsmith.RunParam, 0)
// 构建当前运行参数
run := langsmith.RunParam{
ID: langsmith.String(r.id),
Name: langsmith.String(r.name),
RunType: langsmith.F(r.runType),
SessionName: langsmith.String(r.tracer.projectName),
TraceID: langsmith.String(r.traceID),
DottedOrder: langsmith.String(r.dottedOrder),
StartTime: langsmith.String(r.startTime.Format(time.RFC3339Nano)),
}
// 设置可选字段
if r.parentRunID != "" {
run.ParentRunID = langsmith.String(r.parentRunID)
}
if !r.endTime.IsZero() {
run.EndTime = langsmith.String(r.endTime.Format(time.RFC3339Nano))
}
if r.inputs != nil {
run.Inputs = langsmith.F(r.inputs)
}
if r.outputs != nil {
run.Outputs = langsmith.F(r.outputs)
}
if r.error != "" {
run.Error = langsmith.String(r.error)
}
if len(r.tags) > 0 {
run.Tags = langsmith.F(r.tags)
}
runs = append(runs, run)
// 🔑 递归收集子运行
for _, child := range r.children {
childRuns := child.collectRuns()
runs = append(runs, childRuns...)
}
return runs
}
批量提交的好处:
❌ 逐个提交(效率低):
submit(root) → 网络请求 1
submit(child1) → 网络请求 2
submit(child2) → 网络请求 3
submit(child3) → 网络请求 4
总计:4 次网络请求
✅ 批量提交(我们的方案):
collectRuns() → [root, child1, child2, child3]
IngestBatch() → 网络请求 1
总计:1 次网络请求
6. Context 传递 —— 跨节点传递追踪信息
// ContextKey 用于在 context 中存储跟踪信息的键类型
type ContextKey string
const (
TraceContextKey ContextKey = "langsmith_run"
)
// WithRun 将运行信息存入 context
func WithRun(ctx context.Context, run *Run) context.Context {
return context.WithValue(ctx, TraceContextKey, run)
}
// GetRun 从 context 获取当前运行
func GetRun(ctx context.Context) *Run {
run, ok := ctx.Value(TraceContextKey).(*Run)
if !ok {
return nil
}
return run
}
为什么用 Context 传递?
工作流执行流程:
graph.Run(ctx, state)
│
├── RunNode(ctx, "tool_decision", state)
│ └── 需要在这里拿到父级 Run 创建子 Run
│
├── RunNode(ctx, "tool_execution", state)
│ └── 同上
│
└── RunNode(ctx, "final_response", state)
└── 同上
Context 是 Go 中跨函数传递"请求级别"数据的标准方式
每个节点函数通过 tracing.GetRun(ctx) 获取父级 Run
工作流集成
1. 工作流入口 —— 创建根追踪
// workflow/graph.go
// Invoke 执行工作流
func (cw *CompiledWorkflow) Invoke(ctx context.Context, state *AgentState) error {
// 如果启用了跟踪,创建顶级运行
if cw.tracer != nil && cw.tracer.IsEnabled() {
run := cw.tracer.StartRun("workflow_invoke", langsmith.RunRunTypeChain,
map[string]interface{}{
"user_message": state.PendingUserMessage,
"phase": string(state.Phase),
}, []string{"workflow", "agent"})
// 将运行信息注入 context
ctx = tracing.WithRun(ctx, run)
// 执行工作流
err := cw.graph.Run(ctx, state)
// 记录输出
run.SetOutputs(map[string]interface{}{
"response": state.GeneratedResponse,
"phase": string(state.Phase),
"tool_iterations": state.ToolIterations,
"has_tool_calls": state.HasToolCalls,
"all_tools_ok": state.AllToolsOK,
})
if err != nil {
run.SetError(err)
}
// 🔑 结束运行并提交到 LangSmith
if endErr := run.End(ctx); endErr != nil {
fmt.Printf("[Tracing] Failed to submit trace: %v\n", endErr)
}
return err
}
// 未启用追踪,正常执行
return cw.graph.Run(ctx, state)
}
追踪的生命周期:
1. StartRun() ← 创建根节点,记录开始时间
2. WithRun(ctx, run) ← 注入 context,传递给子节点
3. graph.Run() ← 执行工作流,各节点自动创建子追踪
4. SetOutputs() ← 记录输出
5. End() ← 结束并批量提交到 LangSmith
2. 工具决策节点 —— 追踪 LLM 调用
// workflow/nodes.go
func createToolDecisionNode(deps *NodeDependencies) NodeFunc {
return func(ctx context.Context, state *AgentState) error {
// ... 准备消息和工具列表 ...
// 🔑 创建 LLM 调用的追踪
var llmRun *tracing.Run
parentRun := tracing.GetRun(ctx)
if parentRun != nil {
llmRun = parentRun.CreateChild("tool_decision_llm",
langsmith.RunRunTypeLlm, map[string]interface{}{
"messages": messagesToStrings(messages),
})
}
// 调用 LLM
response, err := deps.LLM.GenerateContent(ctx, messages,
llms.WithTools(langChainTools))
if err != nil {
if llmRun != nil {
llmRun.SetError(err) // 记录错误
}
return fmt.Errorf("LLM tool decision failed: %w", err)
}
choice := response.Choices[0]
// 🔑 记录 LLM 输出
if llmRun != nil {
llmRun.SetOutputs(map[string]interface{}{
"content": choice.Content,
"tool_calls": len(choice.ToolCalls),
})
}
// ... 后续处理 ...
}
}
3. 工具执行节点 —— 追踪每个工具调用
func createToolExecutionNode(deps *NodeDependencies) NodeFunc {
return func(ctx context.Context, state *AgentState) error {
// 获取父级追踪
parentRun := tracing.GetRun(ctx)
for _, toolCall := range state.PendingTools {
toolName := toolCall.FunctionCall.Name
toolArgs := toolCall.FunctionCall.Arguments
toolID := toolCall.ID
// 🔑 为每个工具调用创建独立的追踪
var toolRun *tracing.Run
if parentRun != nil {
toolRun = parentRun.CreateChild(toolName,
langsmith.RunRunTypeTool, map[string]interface{}{
"tool_name": toolName,
"arguments": toolArgs,
"tool_id": toolID,
})
}
// 执行工具
result, err := deps.ToolRegistry.ExecuteTool(ctx, toolName, toolArgs)
// 🔑 记录工具执行结果
if err != nil {
state.AllToolsOK = false
if toolRun != nil {
toolRun.SetError(err)
toolRun.SetOutputs(map[string]interface{}{
"error": err.Error(),
})
}
} else {
if toolRun != nil {
toolRun.SetOutputs(map[string]interface{}{
"result": result,
})
}
}
// ... 添加工具结果到状态 ...
}
return nil
}
}
4. RAG 回退节点 —— 追踪检索操作
func createRAGFallbackNode(deps *NodeDependencies) NodeFunc {
return func(ctx context.Context, state *AgentState) error {
parentRun := tracing.GetRun(ctx)
// 🔑 创建 RAG 检索的追踪
var ragRun *tracing.Run
if parentRun != nil {
ragRun = parentRun.CreateChild("rag_retrieval",
langsmith.RunRunTypeRetriever, map[string]interface{}{
"query": query,
})
}
// 执行 RAG 检索
context, err := deps.RAGRetriever.GetRelevantContext(query, 5)
if err == nil && context != "" {
state.RAGContext = context
if ragRun != nil {
ragRun.SetOutputs(map[string]interface{}{
"context": context,
})
}
} else {
if ragRun != nil {
if err != nil {
ragRun.SetError(err)
}
ragRun.SetOutputs(map[string]interface{}{
"context": "",
"note": "无结果",
})
}
}
// 🔑 创建 LLM 生成回复的追踪
var llmRun *tracing.Run
if parentRun != nil {
llmRun = parentRun.CreateChild("rag_llm_response",
langsmith.RunRunTypeLlm, map[string]interface{}{
"messages": messagesToStrings(messages),
"rag_context": ragContext,
})
}
// ... 调用 LLM 生成回复 ...
}
}
配置与接入
1. 配置结构
// config/config.go
// LangSmithConfig 保存 LangSmith 跟踪配置
type LangSmithConfig struct {
APIKey string // LangSmith API 密钥
ProjectName string // LangSmith 项目名称
Enabled bool // 是否启用跟踪
}
// Config 中新增 LangSmith 配置
type Config struct {
API APIConfig
ChromaDB ChromaDBConfig
Embedding rag.EmbeddingConfig
LangSmith LangSmithConfig // 🆕
}
2. 环境变量配置
# .env 文件
LANGSMITH_API_KEY=lsv2_pt_xxxxxxxxxxxxx # LangSmith API 密钥
LANGSMITH_PROJECT=ai-agent-prod # 项目名称
LANGSMITH_ENABLED=true # 是否启用
// 从环境变量加载
LangSmith: LangSmithConfig{
APIKey: os.Getenv("LANGSMITH_API_KEY"),
ProjectName: os.Getenv("LANGSMITH_PROJECT"),
Enabled: os.Getenv("LANGSMITH_ENABLED") == "true",
},
3. 初始化流程
// agent/agent.go
func NewChatAgentWithRAG(...) (*ChatAgent, error) {
// ... 其他初始化 ...
// 初始化跟踪器
var tracer *tracing.Tracer
if config.LangSmith.Enabled {
tracer = tracing.NewTracer(tracing.Config{
APIKey: config.LangSmith.APIKey,
ProjectName: config.LangSmith.ProjectName,
Enabled: config.LangSmith.Enabled,
})
agent.tracer = tracer
}
// 初始化工作流代理(带跟踪器)
workflowAgent, err := workflow.NewWorkflowAgentWithTracer(
agent.llmProvider.GetLLM(), retriever, tracer)
// ...
}
4. 使用 LangSmith 的前置步骤
# 1. 注册 LangSmith 账号
# 访问 https://smith.langchain.com
# 2. 创建 API Key
# Settings → API Keys → Create API Key
# 3. 创建项目
# Projects → New Project → 输入项目名称
# 4. 配置环境变量
export LANGSMITH_API_KEY="lsv2_pt_xxxxxxxxxxxxx"
export LANGSMITH_PROJECT="ai-agent-prod"
export LANGSMITH_ENABLED="true"
# 5. 安装 Go SDK
go get github.com/langchain-ai/langsmith-go
追踪详情视图(调用树)

设计原则与最佳实践
1. 追踪不影响主流程
// ✅ 好的做法:追踪失败不影响业务
if endErr := run.End(ctx); endErr != nil {
fmt.Printf("[Tracing] Failed to submit trace: %v\n", endErr)
// 不 return err!继续执行
}
// ✅ 好的做法:空操作模式
func (r *Run) SetOutputs(outputs map[string]interface{}) {
if !r.tracer.enabled {
return // 未启用时直接返回,零开销
}
// ...
}
2. 防御性编程
// ✅ 每次使用前都检查 nil
var llmRun *tracing.Run
parentRun := tracing.GetRun(ctx)
if parentRun != nil {
llmRun = parentRun.CreateChild(...)
}
// 使用时也检查
if llmRun != nil {
llmRun.SetOutputs(...)
}
3. 有意义的输入输出
// ❌ 不好:信息太少
run.SetOutputs(map[string]interface{}{
"ok": true,
})
// ✅ 好:记录关键信息
run.SetOutputs(map[string]interface{}{
"response": state.GeneratedResponse,
"phase": string(state.Phase),
"tool_iterations": state.ToolIterations,
"has_tool_calls": state.HasToolCalls,
"all_tools_ok": state.AllToolsOK,
})
4. 线程安全
// Run 使用 sync.Mutex 保护并发访问
type Run struct {
// ...
mu sync.Mutex
}
func (r *Run) SetOutputs(outputs map[string]interface{}) {
r.mu.Lock()
defer r.mu.Unlock()
r.outputs = outputs
}
func (r *Run) CreateChild(...) *Run {
r.mu.Lock()
defer r.mu.Unlock()
// ...
r.children = append(r.children, child)
return child
}
LangSmith vs 传统日志
对比表
| 维度 | 传统日志 (log.Printf) | LangSmith |
|---|---|---|
| 结构 | 扁平的文本行 | 树形调用链 |
| 关联性 | 手动 grep 串联 | 自动关联父子关系 |
| 可视化 | 终端文本 | Web 面板,图形化 |
| 输入输出 | 手动格式化 | 结构化 JSON |
| 耗时分析 | 手动计算时间差 | 自动统计每步耗时 |
| 错误追踪 | 散落在各处 | 集中展示,一目了然 |
| 历史记录 | 日志文件轮转 | 持久化存储,随时查看 |
| 团队协作 | 各看各的日志 | 统一面板,共享分析 |
代码对比
// ❌ 传统日志方式
log.Printf("[ToolDecision] LLM 请求调用 %d 个工具", len(choice.ToolCalls))
log.Printf("[ToolExecution] 执行工具: %s, 参数: %s", toolName, toolArgs)
log.Printf("[ToolExecution] 工具 %s 执行成功: %s", toolName, result)
// 这些日志分散在不同地方,难以串联成完整的调用链
// ✅ LangSmith 追踪方式
parentRun := tracing.GetRun(ctx)
toolRun := parentRun.CreateChild(toolName, langsmith.RunRunTypeTool, inputs)
// 执行工具...
toolRun.SetOutputs(outputs)
// 自动形成调用树,在面板中一目了然
总结
本次更新解决了什么?
| 问题 | 解决方案 |
|---|---|
| Agent 是黑盒 | 全链路追踪,每步可见 |
| 错误难定位 | 追踪树 + 错误标记 |
| 性能难优化 | 每步耗时统计 |
| 日志难串联 | 父子关系自动关联 |
LangSmith 集成的核心要点
- 树形追踪:根节点 → 子节点,自动形成调用链
- Context 传递:通过 Go 的 context 机制跨节点传递追踪信息
- 批量提交:递归收集所有 Run,一次性上报
- 优雅降级:未启用时零开销,追踪失败不影响业务
- dottedOrder:LangSmith 特有的层级排序机制,必须正确实现
Agent 进化路线
Level 1: 基础对话 ✅
↓
Level 2: 智能记忆 ✅
↓
Level 3: RAG 知识库 ✅
↓
Level 4: 框架重构 + Rerank ✅
↓
Level 5: Tool Use 工具调用 ✅
↓
Level 6: LangGraph 工作流 ✅
↓
Level 7: LangSmith 可观测性 ✅ ← 当前
↓
Level 8: 多 Agent 协作
何时需要 LangSmith?
| 场景 | 推荐方案 |
|---|---|
| 本地开发调试 | log.Printf 够用 |
| 简单单步 Agent | log.Printf 够用 |
| 多步工作流 Agent | LangSmith ✅ |
| 生产环境监控 | LangSmith ✅ |
| 团队协作排错 | LangSmith ✅ |
| Agent 行为分析 | LangSmith ✅ |
作者注:可观测性是 AI Agent 从"玩具"走向"生产"的关键一步。没有追踪的 Agent 就像没有仪表盘的汽车——你不知道它在干什么,更不知道它为什么出错。LangSmith 让你拥有了"上帝视角",每一次 LLM 决策、每一次工具调用、每一次检索操作,都清清楚楚。当你的 Agent 行为异常时,打开 LangSmith 面板,答案就在那里。
Happy Coding! 🚀
更多推荐



所有评论(0)