一、为什么需要决策链路可视化?
Agent 与传统 API 最大的区别在于:它有自己的思考过程。
一个典型的 ReAct 循环是这样的:
思考:用户问北京天气,我需要调用 get_weather 工具
行动:调用 get_weather(city="北京")
观察:{"temperature": 22, "condition": "晴"}
思考:还需要补充湿度信息
行动:调用 get_humidity(city="北京")
观察:{"humidity": 45}
思考:信息完整了,可以生成回答
回答:北京今天 22°C,湿度 45%,天气晴朗
如果没有可视化,你只能看到输入和输出,中间的推理过程完全是个黑盒。问题来了:
- Agent 为什么选择了这个工具? ------ 推理链路不透明
- Agent 为什么陷入了死循环? ------ 看不到重复的模式
- Agent 为什么给出了错误答案? ------ 无法定位是哪一步推理出错
- 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。所有计算在浏览器完成,文件不上服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。