第5讲:Agent 决策链路可视化——让 Agent 的思考过程透明化

一、为什么需要决策链路可视化?

Agent 与传统 API 最大的区别在于:它有自己的思考过程

一个典型的 ReAct 循环是这样的:

复制代码
思考:用户问北京天气,我需要调用 get_weather 工具
行动:调用 get_weather(city="北京")
观察:{"temperature": 22, "condition": "晴"}
思考:还需要补充湿度信息
行动:调用 get_humidity(city="北京")
观察:{"humidity": 45}
思考:信息完整了,可以生成回答
回答:北京今天 22°C,湿度 45%,天气晴朗

如果没有可视化,你只能看到输入和输出,中间的推理过程完全是个黑盒。问题来了:

  1. Agent 为什么选择了这个工具? ------ 推理链路不透明
  2. Agent 为什么陷入了死循环? ------ 看不到重复的模式
  3. Agent 为什么给出了错误答案? ------ 无法定位是哪一步推理出错
  4. Agent 花了多少步才完成任务? ------ 效率无从评估

决策链路可视化就是要把这个黑盒变成玻璃盒。


二、核心概念

概念 说明 示例
Step 决策链路上的一个节点 思考、行动、观察
Thought LLM 的内部推理文本 "用户需要天气信息..."
Action Agent 执行的操作 调用工具、查询数据库
Observation 行动的返回结果 工具返回的数据
Chain Step 的有序序列 Thought→Action→Observation→Thought...
Branch 条件分支 如果工具失败则重试,否则继续
Loop 循环检测 连续 3 次相同的 Action+Observation

三、Go 实现:Agent 决策链路记录与可视化

复制代码
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"math"
	"os"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

// ---- 核心数据结构 ----

// StepType 表示决策步骤的类型
type StepType string

const (
	StepThought      StepType = "thought"
	StepAction       StepType = "action"
	StepObservation  StepType = "observation"
	StepFinalAnswer  StepType = "final_answer"
	StepError        StepType = "error"
	StepSystem       StepType = "system"
)

// DecisionStep 表示决策链上的一个步骤
type DecisionStep struct {
	ID            string                 `json:"id"`
	SequenceNum   int                    `json:"sequence_num"`
	Type          StepType               `json:"type"`
	Timestamp     time.Time              `json:"timestamp"`
	DurationMs    int64                  `json:"duration_ms"`

	// 内容
	Content       string                 `json:"content"`
	ToolName      string                 `json:"tool_name,omitempty"`
	ToolArgs      map[string]interface{} `json:"tool_args,omitempty"`
	ToolResult    string                 `json:"tool_result,omitempty"`

	// 元数据
	TokenCount    int                    `json:"token_count,omitempty"`
	ModelUsed     string                 `json:"model_used,omitempty"`
	Confidence    float64                `json:"confidence,omitempty"` // 0-1

	// 状态
	Status        string                 `json:"status"` // pending, running, success, failed
	ErrorMessage  string                 `json:"error_message,omitempty"`

	// 父子关系
	ParentStepID  string                 `json:"parent_step_id,omitempty"`
	ChildSteps    []string               `json:"child_steps,omitempty"`
}

// DecisionChain 表示完整的决策链路
type DecisionChain struct {
	ChainID       string                 `json:"chain_id"`
	SessionID     string                 `json:"session_id"`
	AgentID       string                 `json:"agent_id"`
	UserQuery     string                 `json:"user_query"`
	FinalAnswer   string                 `json:"final_answer,omitempty"`
	StartedAt     time.Time              `json:"started_at"`
	CompletedAt   time.Time              `json:"completed_at,omitempty"`
	TotalDurationMs int64                `json:"total_duration_ms"`
	TotalSteps    int                    `json:"total_steps"`
	TotalTokens   int                    `json:"total_tokens"`
	IsComplete    bool                   `json:"is_complete"`
	IsSuccess     bool                   `json:"is_success"`

	Steps         []*DecisionStep        `json:"steps"`
	StepMap       map[string]*DecisionStep `json:"-"`

	// 分析结果
	Analysis      *ChainAnalysis         `json:"analysis,omitempty"`
}

type ChainAnalysis struct {
	LoopsDetected     int                `json:"loops_detected"`
	RedundantSteps    int                `json:"redundant_steps"`
	AverageConfidence float64            `json:"average_confidence"`
	EfficiencyScore   float64            `json:"efficiency_score"` // 0-100
	Issues            []ChainIssue       `json:"issues"`
}

type ChainIssue struct {
	Severity  string `json:"severity"` // warning, error
	StepID    string `json:"step_id"`
	Message   string `json:"message"`
}

// ---- 决策链路记录器 ----
type ChainRecorder struct {
	mu           sync.Mutex
	activeChains map[string]*DecisionChain
	completedChains []*DecisionChain
	maxChains    int

	// 回调
	onStepAdded   func(chainID string, step *DecisionStep)
	onChainComplete func(chain *DecisionChain)
}

func NewChainRecorder(maxChains int) *ChainRecorder {
	return &ChainRecorder{
		activeChains:    make(map[string]*DecisionChain),
		completedChains: make([]*DecisionChain, 0, maxChains),
		maxChains:       maxChains,
	}
}

func (cr *ChainRecorder) OnStepAdded(fn func(string, *DecisionStep)) {
	cr.onStepAdded = fn
}

func (cr *ChainRecorder) OnChainComplete(fn func(*DecisionChain)) {
	cr.onChainComplete = fn
}

// StartChain 开始记录一个新的决策链路
func (cr *ChainRecorder) StartChain(sessionID, agentID, userQuery string) string {
	chain := &DecisionChain{
		ChainID:   generateChainID(),
		SessionID: sessionID,
		AgentID:   agentID,
		UserQuery: userQuery,
		StartedAt: time.Now(),
		Steps:     make([]*DecisionStep, 0),
		StepMap:   make(map[string]*DecisionStep),
	}

	cr.mu.Lock()
	cr.activeChains[chain.ChainID] = chain
	cr.mu.Unlock()

	return chain.ChainID
}

// AddStep 添加一个决策步骤
func (cr *ChainRecorder) AddStep(chainID string, stepType StepType, opts ...StepOption) *DecisionStep {
	cr.mu.Lock()
	chain, exists := cr.activeChains[chainID]
	if !exists {
		cr.mu.Unlock()
		return nil
	}

	step := &DecisionStep{
		ID:          generateStepID(),
		SequenceNum: len(chain.Steps) + 1,
		Type:        stepType,
		Timestamp:   time.Now(),
		Status:      "running",
	}

	for _, opt := range opts {
		opt(step)
	}

	chain.Steps = append(chain.Steps, step)
	chain.StepMap[step.ID] = step
	chain.TotalSteps = len(chain.Steps)

	cr.mu.Unlock()

	// 触发回调
	if cr.onStepAdded != nil {
		cr.onStepAdded(chainID, step)
	}

	return step
}

// CompleteStep 标记步骤完成
func (cr *ChainRecorder) CompleteStep(chainID, stepID string, status string, opts ...StepOption) {
	cr.mu.Lock()
	chain, exists := cr.activeChains[chainID]
	if !exists {
		cr.mu.Unlock()
		return
	}

	step, exists := chain.StepMap[stepID]
	if !exists {
		cr.mu.Unlock()
		return
	}

	step.Status = status
	step.DurationMs = time.Since(step.Timestamp).Milliseconds()

	for _, opt := range opts {
		opt(step)
	}

	cr.mu.Unlock()
}

// CompleteChain 完成决策链路
func (cr *ChainRecorder) CompleteChain(chainID string, finalAnswer string, success bool) {
	cr.mu.Lock()
	chain, exists := cr.activeChains[chainID]
	if !exists {
		cr.mu.Unlock()
		return
	}

	chain.CompletedAt = time.Now()
	chain.TotalDurationMs = time.Since(chain.StartedAt).Milliseconds()
	chain.FinalAnswer = finalAnswer
	chain.IsComplete = true
	chain.IsSuccess = success

	// 计算总 Token 消耗
	totalTokens := 0
	for _, step := range chain.Steps {
		totalTokens += step.TokenCount
	}
	chain.TotalTokens = totalTokens

	// 分析链路
	chain.Analysis = analyzeChain(chain)

	delete(cr.activeChains, chainID)
	cr.completedChains = append(cr.completedChains, chain)
	if len(cr.completedChains) > cr.maxChains {
		cr.completedChains = cr.completedChains[1:]
	}
	cr.mu.Unlock()

	// 触发回调
	if cr.onChainComplete != nil {
		cr.onChainComplete(chain)
	}
}

// GetChain 获取指定链路
func (cr *ChainRecorder) GetChain(chainID string) *DecisionChain {
	cr.mu.Lock()
	defer cr.mu.Unlock()

	if chain, exists := cr.activeChains[chainID]; exists {
		return chain
	}

	for _, chain := range cr.completedChains {
		if chain.ChainID == chainID {
			return chain
		}
	}
	return nil
}

// GetRecentChains 获取最近的链路列表
func (cr *ChainRecorder) GetRecentChains(n int) []*DecisionChain {
	cr.mu.Lock()
	defer cr.mu.Unlock()

	result := make([]*DecisionChain, 0, n)
	result = append(result, cr.completedChains...)

	// 按时间倒序排列
	sort.Slice(result, func(i, j int) bool {
		return result[i].StartedAt.After(result[j].StartedAt)
	})

	if len(result) > n {
		result = result[:n]
	}

	return result
}

// ---- 步骤选项 ----
type StepOption func(*DecisionStep)

func WithContent(content string) StepOption {
	return func(s *DecisionStep) {
		s.Content = content
	}
}

func WithTool(name string, args map[string]interface{}) StepOption {
	return func(s *DecisionStep) {
		s.ToolName = name
		s.ToolArgs = args
	}
}

func WithToolResult(result string) StepOption {
	return func(s *DecisionStep) {
		s.ToolResult = result
	}
}

func WithTokenCount(count int) StepOption {
	return func(s *DecisionStep) {
		s.TokenCount = count
	}
}

func WithModel(model string) StepOption {
	return func(s *DecisionStep) {
		s.ModelUsed = model
	}
}

func WithConfidence(confidence float64) StepOption {
	return func(s *DecisionStep) {
		s.Confidence = confidence
	}
}

func WithError(errMsg string) StepOption {
	return func(s *DecisionStep) {
		s.ErrorMessage = errMsg
	}
}

func WithParent(parentID string) StepOption {
	return func(s *DecisionStep) {
		s.ParentStepID = parentID
	}
}

// ---- 链路分析 ----
func analyzeChain(chain *DecisionChain) *ChainAnalysis {
	analysis := &ChainAnalysis{
		Issues: make([]ChainIssue, 0),
	}

	if len(chain.Steps) == 0 {
		return analysis
	}

	// 1. 循环检测
	analysis.LoopsDetected = detectLoops(chain)
	if analysis.LoopsDetected > 0 {
		analysis.Issues = append(analysis.Issues, ChainIssue{
			Severity: "warning",
			Message:  fmt.Sprintf("检测到 %d 个可能的循环", analysis.LoopsDetected),
		})
	}

	// 2. 冗余步骤检测
	analysis.RedundantSteps = detectRedundancy(chain)
	if analysis.RedundantSteps > 0 {
		analysis.Issues = append(analysis.Issues, ChainIssue{
			Severity: "warning",
			Message:  fmt.Sprintf("检测到 %d 个冗余步骤", analysis.RedundantSteps),
		})
	}

	// 3. 平均置信度
	var totalConfidence float64
	var confidenceCount int
	for _, step := range chain.Steps {
		if step.Confidence > 0 {
			totalConfidence += step.Confidence
			confidenceCount++
		}
	}
	if confidenceCount > 0 {
		analysis.AverageConfidence = totalConfidence / float64(confidenceCount)
	}

	// 4. 效率评分
	analysis.EfficiencyScore = calculateEfficiency(chain)

	// 5. 其他问题
	for _, step := range chain.Steps {
		if step.Status == "failed" {
			analysis.Issues = append(analysis.Issues, ChainIssue{
				Severity: "error",
				StepID:   step.ID,
				Message:  fmt.Sprintf("步骤 %d (%s) 失败: %s", step.SequenceNum, step.Type, step.ErrorMessage),
			})
		}
	}

	// 检查是否有最终答案
	hasFinalAnswer := false
	for _, step := range chain.Steps {
		if step.Type == StepFinalAnswer {
			hasFinalAnswer = true
			break
		}
	}
	if !hasFinalAnswer && chain.IsComplete {
		analysis.Issues = append(analysis.Issues, ChainIssue{
			Severity: "error",
			Message:  "链路已完成但没有最终答案",
		})
	}

	return analysis
}

func detectLoops(chain *DecisionChain) int {
	// 检测连续相同的 Action+Observation 模式
	loopCount := 0
	patternWindow := 3 // 连续 3 次相同视为循环

	for i := 0; i < len(chain.Steps)-patternWindow*2; i++ {
		matched := true
		for j := 0; j < patternWindow; j++ {
			s1 := chain.Steps[i+j]
			s2 := chain.Steps[i+patternWindow+j]
			if s1.Type != s2.Type || s1.ToolName != s2.ToolName {
				matched = false
				break
			}
		}
		if matched {
			loopCount++
			i += patternWindow * 2 // 跳过已检测到的循环
		}
	}

	return loopCount
}

func detectRedundancy(chain *DecisionChain) int {
	// 检测重复的工具调用(相同参数)
	toolCalls := make(map[string]bool)
	redundant := 0

	for _, step := range chain.Steps {
		if step.Type == StepAction && step.ToolName != "" {
			key := fmt.Sprintf("%s-%v", step.ToolName, step.ToolArgs)
			if toolCalls[key] {
				redundant++
			}
			toolCalls[key] = true
		}
	}

	return redundant
}

func calculateEfficiency(chain *DecisionChain) float64 {
	if len(chain.Steps) == 0 {
		return 0
	}

	score := 100.0

	// 步骤太多扣分
	if len(chain.Steps) > 10 {
		score -= float64(len(chain.Steps)-10) * 5
	}

	// 耗时太长扣分
	if chain.TotalDurationMs > 30000 {
		score -= 10
	}

	// 有失败步骤扣分
	for _, step := range chain.Steps {
		if step.Status == "failed" {
			score -= 15
			break
		}
	}

	// 有循环扣分
	if chain.Analysis != nil && chain.Analysis.LoopsDetected > 0 {
		score -= float64(chain.Analysis.LoopsDetected) * 20
	}

	return math.Max(0, math.Min(100, score))
}

// ---- HTML 可视化生成 ----
type ChainVisualizer struct {
	templates *template.Template
}

func NewChainVisualizer() *ChainVisualizer {
	return &ChainVisualizer{}
}

func (cv *ChainVisualizer) RenderHTML(chain *DecisionChain) (string, error) {
	tmpl := `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Agent 决策链路可视化</title>
<style>
  body { font-family: 'Segoe UI', sans-serif; background: #f5f5f5; margin: 20px; }
  .chain-container { max-width: 960px; margin: 0 auto; }
  .header { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
  .header h1 { margin: 0 0 10px 0; color: #333; }
  .meta { display: flex; gap: 20px; flex-wrap: wrap; }
  .meta-item { background: #e8f4fd; padding: 4px 12px; border-radius: 4px; font-size: 14px; }
  .step { background: white; border-radius: 8px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; }
  .step-header { padding: 12px 16px; cursor: pointer; display: flex; align-items: center; gap: 12px; }
  .step-number { width: 36px; height: 26px; background: #e0e0e0; border-radius: 999px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 13px; color: #666; }
  .step-type { font-weight: 600; font-size: 14px; min-width: 70px; }
  .step-status { font-size: 12px; padding: 2px 8px; border-radius: 4px; }
  .step-status.success { background: #e8f5e9; color: #2e7d32; }
  .step-status.failed { background: #ffebee; color: #c62828; }
  .step-status.running { background: #fff3e0; color: #ef6c00; }
  .step-body { padding: 0 16px 16px 52px; display: none; }
  .step.open .step-body { display: block; }
  .thought-content { background: #fafafa; padding: 12px; border-radius: 6px; border-left: 3px solid #1976d2; margin-top: 8px; font-size: 14px; line-height: 1.6; white-space: pre-wrap; }
  .tool-call { background: #f3e5f5; padding: 12px; border-radius: 6px; border-left: 3px solid #7b1fa2; margin-top: 8px; }
  .tool-result { background: #e8f5e9; padding: 12px; border-radius: 6px; border-left: 3px solid #388e3c; margin-top: 8px; }
  .error-info { background: #ffebee; padding: 12px; border-radius: 6px; border-left: 3px solid #d32f2f; margin-top: 8px; }
  .arrow-down { transform: rotate(0deg); transition: transform 0.2s; }
  .step.open .arrow-down { transform: rotate(180deg); }
  .analysis-panel { background: white; border-radius: 8px; padding: 20px; margin-top: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
  .issue-warning { background: #fff3e0; padding: 8px 12px; border-radius: 4px; margin: 4px 0; }
  .issue-error { background: #ffebee; padding: 8px 12px; border-radius: 4px; margin: 4px 0; }
  .timeline { position: relative; padding-left: 30px; }
  .timeline::before { content: ''; position: absolute; left: 19px; top: 44px; bottom: 130px; width: 2px; background: #ddd; }
  .type-thought { color: #1976d2; }
  .type-action { color: #7b1fa2; }
  .type-observation { color: #388e3c; }
  .type-final_answer { color: #f57c00; }
  .type-error { color: #d32f2f; }
</style>
</head>
<body>
<div class="chain-container">
  <div class="header">
    <h1>🤖 Agent 决策链路</h1>
    <div class="meta">
      <span class="meta-item">🆔 {{.ChainID}}</span>
      <span class="meta-item">🤖 {{.AgentID}}</span>
      <span class="meta-item">💬 {{.UserQuery}}</span>
      <span class="meta-item">⏱️ {{.TotalDurationMs}}ms</span>
      <span class="meta-item">📊 {{.TotalSteps}} 步</span>
      <span class="meta-item">🔤 {{.TotalTokens}} tokens</span>
      <span class="meta-item">{{if .IsSuccess}}✅ 成功{{else}}❌ 失败{{end}}</span>
    </div>
  </div>

  {{if .Analysis}}
  <div class="analysis-panel">
    <h3>📋 链路分析</h3>
    <div style="display:flex;gap:20px;margin:10px 0;">
      <div>效率评分: <strong>{{printf "%.1f" .Analysis.EfficiencyScore}}</strong>/100</div>
      <div>平均置信度: <strong>{{printf "%.2f" .Analysis.AverageConfidence}}</strong></div>
      <div>循环检测: <strong>{{.Analysis.LoopsDetected}}</strong></div>
      <div>冗余步骤: <strong>{{.Analysis.RedundantSteps}}</strong></div>
    </div>
    {{range .Analysis.Issues}}
    <div class="issue-{{.Severity}}">
      {{if eq .Severity "error"}}🔴{{else}}🟡{{end}}
      {{.Message}}
    </div>
    {{end}}
  </div>
  {{end}}

  <div class="timeline">
    {{range .Steps}}
    <div class="step" onclick="this.classList.toggle('open')">
      <div class="step-header">
        <div class="step-number">{{.SequenceNum}}</div>
        <div class="step-type type-{{.Type}}">{{.Type}}</div>
        <div style="flex:1;font-size:14px;color:#666;">
          {{if eq .Type "thought"}}🧠 思考{{end}}
          {{if eq .Type "action"}}🔧 调用 {{.ToolName}}{{end}}
          {{if eq .Type "observation"}}👁️ 观察结果{{end}}
          {{if eq .Type "final_answer"}}💬 最终回答{{end}}
          {{if eq .Type "error"}}❌ 错误{{end}}
        </div>
        <span class="step-status {{.Status}}">{{.Status}}</span>
        <span style="font-size:12px;color:#999;">{{.DurationMs}}ms</span>
        <span class="arrow-down" style="font-size:18px;">▼</span>
      </div>
      <div class="step-body">
        {{if .Content}}
        <div class="thought-content">{{.Content}}</div>
        {{end}}
        {{if .ToolName}}
        <div class="tool-call">
          <strong>工具:</strong> {{.ToolName}}<br>
          <strong>参数:</strong> {{toJSON .ToolArgs}}
        </div>
        {{end}}
        {{if .ToolResult}}
        <div class="tool-result"><strong>结果:</strong> {{.ToolResult}}</div>
        {{end}}
        {{if .ErrorMessage}}
        <div class="error-info"><strong>错误:</strong> {{.ErrorMessage}}</div>
        {{end}}
        {{if .ModelUsed}}
        <div style="margin-top:8px;font-size:12px;color:#999;">模型: {{.ModelUsed}} | Token: {{.TokenCount}} | 置信度: {{printf "%.2f" .Confidence}}</div>
        {{end}}
      </div>
    </div>
    {{end}}
  </div>

  {{if .FinalAnswer}}
  <div class="analysis-panel" style="background:#fff3e0;">
    <h3>💬 最终回答</h3>
    <p style="font-size:15px;line-height:1.6;">{{.FinalAnswer}}</p>
  </div>
  {{end}}
</div>
<script>
  // 自动展开前两步
  document.querySelectorAll('.step').forEach((el, i) => {
    if (i < 2) el.classList.add('open');
  });
</script>
</body>
</html>
`

	funcs := template.FuncMap{
		"toJSON": func(v interface{}) string {
			b, _ := json.Marshal(v)
			return string(b)
		},
	}

	t, err := template.New("chain").Funcs(funcs).Parse(tmpl)
	if err != nil {
		return "", err
	}

	var buf strings.Builder
	if err := t.Execute(&buf, chain); err != nil {
		return "", err
	}

	return buf.String(), nil
}

// ---- Agent 决策执行器(模拟) ----
type AgentExecutor struct {
	recorder *ChainRecorder
	llm      *MockLLM
	tools    map[string]ToolFunction
}

type MockLLM struct{}

func (m *MockLLM) Think(prompt string) (string, string, map[string]interface{}, error) {
	// 模拟 LLM 的思考过程
	time.Sleep(time.Duration(100+rand.Intn(400)) * time.Millisecond)

	if strings.Contains(prompt, "天气") {
		return "用户想知道天气情况,我需要调用天气查询工具。", "get_weather", map[string]interface{}{
			"city": extractCity(prompt),
		}, nil
	}
	if strings.Contains(prompt, "翻译") {
		return "用户需要翻译,我可以直接回答。", "", nil, nil
	}
	return "让我思考如何处理这个请求。", "", nil, nil
}

type ToolFunction func(args map[string]interface{}) (string, error)

func NewAgentExecutor(recorder *ChainRecorder) *AgentExecutor {
	return &AgentExecutor{
		recorder: recorder,
		llm:      &MockLLM{},
		tools: map[string]ToolFunction{
			"get_weather": func(args map[string]interface{}) (string, error) {
				time.Sleep(200 * time.Millisecond)
				city := args["city"].(string)
				return fmt.Sprintf(`{"city":"%s","temperature":22,"condition":"晴","humidity":45}`, city), nil
			},
			"translate": func(args map[string]interface{}) (string, error) {
				time.Sleep(150 * time.Millisecond)
				return "Translation complete", nil
			},
		},
	}
}

func (ae *AgentExecutor) Execute(ctx context.Context, sessionID, agentID, userQuery string) string {
	chainID := ae.recorder.StartChain(sessionID, agentID, userQuery)

	// 记录用户查询
	ae.recorder.AddStep(chainID, StepSystem, WithContent(fmt.Sprintf("用户查询: %s", userQuery)))

	maxIterations := 10
	for i := 0; i < maxIterations; i++ {
		// 思考步骤
		thoughtStep := ae.recorder.AddStep(chainID, StepThought,
			WithModel("gpt-4o"),
			WithConfidence(0.85),
		)

		thought, action, args, err := ae.llm.Think(userQuery)
		if err != nil {
			ae.recorder.CompleteStep(chainID, thoughtStep.ID, "failed",
				WithError(err.Error()),
			)
			break
		}

		ae.recorder.CompleteStep(chainID, thoughtStep.ID, "success",
			WithContent(thought),
			WithTokenCount(150+i*20),
		)

		// 如果是最终回答(不需要工具)
		if action == "" {
			finalStep := ae.recorder.AddStep(chainID, StepFinalAnswer,
				WithContent(thought),
				WithModel("gpt-4o"),
			)
			ae.recorder.CompleteStep(chainID, finalStep.ID, "success")
			ae.recorder.CompleteChain(chainID, thought, true)
			return chainID
		}

		// 行动步骤
		actionStep := ae.recorder.AddStep(chainID, StepAction,
			WithTool(action, args),
			WithModel("gpt-4o"),
		)

		toolFn, exists := ae.tools[action]
		if !exists {
			ae.recorder.CompleteStep(chainID, actionStep.ID, "failed",
				WithError(fmt.Sprintf("未知工具: %s", action)),
			)
			continue
		}

		result, err := toolFn(args)
		if err != nil {
			ae.recorder.CompleteStep(chainID, actionStep.ID, "failed",
				WithError(err.Error()),
			)
			continue
		}

		ae.recorder.CompleteStep(chainID, actionStep.ID, "success",
			WithToolResult(result),
		)

		// 观察步骤
		obsStep := ae.recorder.AddStep(chainID, StepObservation,
			WithContent(fmt.Sprintf("工具 %s 返回: %s", action, result)),
		)
		ae.recorder.CompleteStep(chainID, obsStep.ID, "success",
			WithToolResult(result),
		)

		// 更新用户查询以包含观察结果
		userQuery = fmt.Sprintf("%s\n观察结果: %s", userQuery, result)
	}

	// 超时或达到最大迭代次数
	ae.recorder.CompleteChain(chainID, "无法完成请求", false)
	return chainID
}

// ---- 辅助函数 ----
var chainIDCounter uint64

func generateChainID() string {
	n := atomic.AddUint64(&chainIDCounter, 1)
	return fmt.Sprintf("chain_%08x", n)
}

var stepIDCounter uint64

func generateStepID() string {
	n := atomic.AddUint64(&stepIDCounter, 1)
	return fmt.Sprintf("step_%08x", n)
}

func extractCity(query string) string {
	cities := []string{"北京", "上海", "广州", "深圳", "杭州"}
	for _, city := range cities {
		if strings.Contains(query, city) {
			return city
		}
	}
	return "北京"
}

// ---- 演示 ----
func main() {
	recorder := NewChainRecorder(100)
	visualizer := NewChainVisualizer()
	executor := NewAgentExecutor(recorder)

	fmt.Println("========== Agent 决策链路可视化演示 ==========\n")

	// 1. 正常执行
	fmt.Println("--- 1. 正常 Agent 执行 ---")
	chainID := executor.Execute(nil, "sess-001", "agent-weather-001", "北京今天天气怎么样?")
	chain := recorder.GetChain(chainID)
	printChainSummary(chain)

	// 2. 显示步骤详情
	fmt.Println("\n--- 2. 步骤详情 ---")
	for _, step := range chain.Steps {
		fmt.Printf("  Step %d [%s] %s\n", step.SequenceNum, step.Type, step.Status)
		if step.Content != "" {
			fmt.Printf("    内容: %s\n", truncateString(step.Content, 80))
		}
		if step.ToolName != "" {
			fmt.Printf("    工具: %s\n", step.ToolName)
		}
		if step.ToolResult != "" {
			fmt.Printf("    结果: %s\n", truncateString(step.ToolResult, 80))
		}
	}

	// 3. 多个并发执行
	fmt.Println("\n--- 3. 多个并发执行 ---")
	queries := []string{
		"上海的天气怎么样?",
		"翻译 hello world 到中文",
		"广州明天会下雨吗?",
	}
	for i, query := range queries {
		executor.Execute(nil, fmt.Sprintf("sess-%03d", i+1), fmt.Sprintf("agent-%03d", i+1), query)
	}

	// 4. 显示最近的链路
	fmt.Println("\n--- 4. 最近链路列表 ---")
	chains := recorder.GetRecentChains(5)
	for _, c := range chains {
		fmt.Printf("  %s | %s | %d 步 | %dms | %s\n",
			c.ChainID, c.UserQuery, c.TotalSteps, c.TotalDurationMs,
			map[bool]string{true: "✅", false: "❌"}[c.IsSuccess])
	}

	// 5. 分析报告
	fmt.Println("\n--- 5. 链路分析 ---")
	analysis := chain.Analysis
	if analysis != nil {
		fmt.Printf("  效率评分: %.1f/100\n", analysis.EfficiencyScore)
		fmt.Printf("  平均置信度: %.2f\n", analysis.AverageConfidence)
		fmt.Printf("  循环检测: %d\n", analysis.LoopsDetected)
		fmt.Printf("  冗余步骤: %d\n", analysis.RedundantSteps)
		for _, issue := range analysis.Issues {
			fmt.Printf("  [%s] %s\n", issue.Severity, issue.Message)
		}
	}

	// 6. 生成 HTML 可视化
	fmt.Println("\n--- 6. 生成 HTML 可视化 ---")
	html, err := visualizer.RenderHTML(chain)
	if err != nil {
		fmt.Printf("生成 HTML 失败: %v\n", err)
	} else {
		filename := fmt.Sprintf("chain_%s.html", chain.ChainID)
		os.WriteFile(filename, []byte(html), 0644)
		fmt.Printf("已生成: %s\n", filename)
	}

	// 7. 实时仪表板
	fmt.Println("\n--- 7. 决策链路仪表板 ---")
	fmt.Println("┌───────────────────────────────────────────────────────────────┐")
	fmt.Println("│              Agent 决策链路实时仪表板                        │")
	fmt.Println("├───────────────────────────────────────────────────────────────┤")
	fmt.Println("│ 最近 1 小时:                                               │")
	fmt.Println("│  总链路: 4  |  成功: 4  |  失败: 0  |  成功率: 100%       │")
	fmt.Println("│  平均步数: 3.5  |  平均耗时: 1450ms  |  平均 Token: 520    │")
	fmt.Println("├───────────────────────────────────────────────────────────────┤")
	fmt.Println("│ 活跃链路: 0                                                │")
	fmt.Println("│ 常见问题:                                                  │")
	fmt.Println("│  ✅ 暂无检测到异常                                         │")
	fmt.Println("└───────────────────────────────────────────────────────────────┘")
}

func printChainSummary(chain *DecisionChain) {
	fmt.Printf("  链路ID: %s\n", chain.ChainID)
	fmt.Printf("  用户查询: %s\n", chain.UserQuery)
	fmt.Printf("  总步数: %d\n", chain.TotalSteps)
	fmt.Printf("  总耗时: %dms\n", chain.TotalDurationMs)
	fmt.Printf("  总 Token: %d\n", chain.TotalTokens)
	fmt.Printf("  状态: %s\n", map[bool]string{true: "✅ 成功", false: "❌ 失败"}[chain.IsSuccess])
	if chain.FinalAnswer != "" {
		fmt.Printf("  最终回答: %s\n", truncateString(chain.FinalAnswer, 100))
	}
}

四、决策链路可视化架构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Agent Runtime                            │
│  ┌────────────┐  ┌────────────┐  ┌────────────────────┐    │
│  │  LLM 调用  │  │  工具执行  │  │  记忆检索          │    │
│  └──────┬─────┘  └──────┬─────┘  └────────┬───────────┘    │
│         │               │                 │                │
│  ┌──────▼───────────────▼─────────────────▼────────────┐   │
│  │              Chain Recorder                        │   │
│  │  记录每一步的 Thought/Action/Observation            │   │
│  └──────────────────────┬─────────────────────────────┘   │
└─────────────────────────┼─────────────────────────────────┘
                          │
┌─────────────────────────▼─────────────────────────────────┐
│                    Storage Layer                           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐    │
│  │  内存缓冲区  │  │  时序数据库  │  │  对象存储    │    │
│  └──────────────┘  └──────────────┘  └──────────────┘    │
└─────────────────────────┬─────────────────────────────────┘
                          │
┌─────────────────────────▼─────────────────────────────────┐
│                    Visualization Layer                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐    │
│  │  HTML 报告   │  │  WebSocket   │  │  Grafana     │    │
│  │  (离线查看)  │  │  (实时推送)  │  │  (仪表板)    │    │
│  └──────────────┘  └──────────────┘  └──────────────┘    │
└───────────────────────────────────────────────────────────┘

五、可视化最佳实践

原则 说明
时间线视图 按时间顺序展示所有步骤,一目了然
颜色编码 不同步骤类型用不同颜色(蓝=思考,紫=行动,绿=观察)
可折叠 默认折叠详细内容,点击展开
状态标识 成功/失败/运行中用不同图标和颜色
耗时显示 每个步骤显示耗时,快速定位瓶颈
链路分析 自动检测循环、冗余、异常模式
搜索过滤 支持按步骤类型、状态、工具名搜索
对比模式 并排对比两个链路的差异

六、异常模式检测

模式 检测规则 严重程度
死循环 连续 3+ 次相同的 Action+Observation 🔴 严重
工具滥用 同一工具被调用 5+ 次 🟡 警告
空转 连续 3+ 次 Thought 没有 Action 🟡 警告
过长链路 步骤数 > 15 🟢 提示
低置信度 连续 2+ 步置信度 < 0.5 🟡 警告
频繁错误 错误率 > 30% 🔴 严重

七、延伸阅读

  • ReAct: Synergizing Reasoning and Acting in Language Models:ReAct 模式的原始论文
  • LangChain Callbacks:LangChain 的回调机制,可用于记录链路
  • OpenAI Function Calling:OpenAI 的工具调用机制
  • Chain-of-Thought Prompting Elicits Reasoning in Large Language Models:CoT 的原始论文
  • Agent Protocol:OpenAI 提出的 Agent 标准化协议

八、下一讲预告

第6讲:告警与阈值体系------在问题发生前发现它

前五讲建立了一套完整的可观测性系统(日志、追踪、指标、LLM 监控、决策链路)。现在我们需要让它主动工作------当系统出现异常时及时通知你。这一讲将实现:多级告警规则引擎、智能阈值(动态基线)、告警抑制与聚合、多渠道通知(钉钉/企业微信/Slack/PagerDuty)、以及告警风暴防护。


🧰 开发之余的小工具推荐

处理 Base64、JSON 格式化、JWT 解析、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top。所有计算在浏览器完成,文件不上服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。

相关推荐
星栈6 小时前
用 Rust 写 Agent 服务 -- adk-rust 上手记
后端·agent
梦在远山后8 小时前
Electron 与 FastAPI 如何完成流式 Agent 对话
python·langchain·agent
浩瀚地学9 小时前
deepagents学习打卡day04
python·agent
wangruofeng9 小时前
9 款主流 AI Agent CLI 对比:安装、版本查询与升级命令
aigc·agent·ai编程
智码看视界10 小时前
开源大模型前沿:科研智能体——Intern-S2-397B ,本地可跑的科学文献 Agent
科研·agent·intern-s2-397b·书生-s2·科学多模态大模型·apache 2.0
墨心@10 小时前
实验 2-6 豆包版复现分析:Agent Skills 从论文生成演示文稿
自然语言处理·nlp·agent·智能体·datawhale共学
机械改造鹅10 小时前
拆解 prime-agent 系列——(2)Python runtime 与 kernel
agent
武子康10 小时前
Codex 后台任务结束了,为什么还不能直接接着用?
人工智能·llm·agent
DeepAgent11 小时前
AI Agent 入门指南(05):Agent 循环——观察、思考、行动
面试·agent