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 集成的核心要点

  1. 树形追踪:根节点 → 子节点,自动形成调用链
  2. Context 传递:通过 Go 的 context 机制跨节点传递追踪信息
  3. 批量提交:递归收集所有 Run,一次性上报
  4. 优雅降级:未启用时零开销,追踪失败不影响业务
  5. 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! 🚀

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐