Skip to content

AI Agent 架构与工程化落地 ​

#Agent · #ReAct · #FunctionCalling · #工具调用 · #状态机 · #多轮对话 · #长期记忆 · #Memory · #多模态Agent · #ComputerUse · #CodeAgent · #Go · #Python

用户在 HTML 页面上点击一个按钮,后端 Agent 如何运转?本专题覆盖 ReAct 智能体循环、严谨的 Function Calling 设计、以及多轮对话状态管理。


什么是 AI Agent? ​

AI Agent(智能体)是一个能够自主感知环境、制定计划、调用工具、执行行动的 AI 系统。与传统的"一问一答"不同,Agent 会经历"思考→行动→观察→思考"的循环。

mermaid
graph TD
    USER["用户任务"] --> AGENT["🤖 AI Agent"]
    AGENT --> THINK["💭 思考<br/>我需要什么工具?"]
    THINK --> ACT["🔧 行动<br/>调用 API / 执行代码"]
    ACT --> OBSERVE["👁️ 观察<br/>分析执行结果"]
    OBSERVE --> THINK
    OBSERVE --> DONE["✅ 完成任务<br/>返回最终答案"]

    style AGENT fill:#9b59b6,color:#fff
    style THINK fill:#f39c12,color:#fff
    style ACT fill:#2ecc71,color:#fff

Agent 的核心能力三角 ​

能力说明技术实现
推理 (Reasoning)理解任务、拆解步骤、制定计划ReAct / CoT / Tree-of-Thought
行动 (Acting)调用工具执行具体操作Function Calling / Tool Use
记忆 (Memory)多轮对话上下文维护Session 管理 / 向量数据库 / 摘要

基于 ReAct 的智能体循环 ​

ReAct 原理 ​

ReAct(Reasoning + Acting)是 Agent 最经典的范式。模型交替进行思考(Thought)→ 行动(Action)→ 观察(Observation),直到完成任务。

mermaid
sequenceDiagram
    participant User as 👤 用户
    participant Agent as 🤖 Agent 核心
    participant LLM as 🧠 LLM 模型
    participant Tool as 🔧 工具/API

    User->>Agent: "帮我查下今天深圳天气,并给我穿衣建议"

    loop ReAct 循环
        Agent->>LLM: 发送 Prompt + 工具列表 + 历史
        LLM-->Agent: Thought: 需要先获取天气
        LLM-->Agent: Action: get_weather("深圳")
        Agent->>Tool: 调用 get_weather API
        Tool-->>Agent: Observation: 多云 28°C

        Agent->>LLM: 追加 Observation 到上下文
        LLM-->Agent: Thought: 天气已获取,可以给出建议
        LLM-->Agent: Action: FINISH("多云28°C,建议穿短袖...")
    end

    Agent-->>User: 返回最终答案

使用 Go 实现 ReAct 状态机 ​

go
package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
)

// ReActState 定义 Agent 在 ReAct 循环中的状态
type ReActState int

const (
	StateIdle        ReActState = iota // 空闲,等待用户输入
	StateThinking                       // 正在推理
	StateActing                         // 正在执行工具调用
	StateObserving                      // 正在观察工具结果
	StateFinished                       // 任务完成
	StateError                          // 出错
)

func (s ReActState) String() string {
	return [...]string{"Idle", "Thinking", "Acting", "Observing", "Finished", "Error"}[s]
}

// ReActStep 表示 ReAct 循环中的一步
// 对应 Prompt 驱动模型输出的结构:
//   Thought: <推理文本>
//   Action: <工具名>(<参数JSON>)
//   或 Action: FINISH(<最终答案>)
type ReActStep struct {
	Thought string `json:"thought"` // 模型的推理过程
	Action  string `json:"action"`  // 工具名 或 "FINISH"
	Input   string `json:"input"`   // 工具参数 (JSON) 或最终答案
}

// Tool 定义 Agent 可调用的工具接口
type Tool interface {
	Name() string
	Description() string
	// JSONSchema 返回工具参数的 JSON Schema,用于 LLM 生成正确参数
	JSONSchema() json.RawMessage
	// Execute 执行工具,返回观测结果
	Execute(ctx context.Context, input json.RawMessage) (string, error)
}

// ReActAgent 实现了 ReAct 智能体循环
type ReActAgent struct {
	llm    LLMClient          // LLM 客户端(OpenAI 兼容接口)
	tools  map[string]Tool    // 已注册的工具
	config AgentConfig        // 配置
	state  ReActState         // 当前状态
	steps  []ReActStep        // 历史步骤记录
}

// AgentConfig Agent 的行为配置
type AgentConfig struct {
	MaxSteps     int    // 最大循环步数,防止无限循环(默认 10)
	Temperature  float32 // LLM 温度(0 = 确定,1 = 创造)
	SystemPrompt string  // 系统提示词
	Verbose      bool    // 是否打印详细日志
}

// NewReActAgent 创建一个新的 ReAct Agent
func NewReActAgent(llm LLMClient, config AgentConfig) *ReActAgent {
	if config.MaxSteps == 0 {
		config.MaxSteps = 10
	}
	return &ReActAgent{
		llm:    llm,
		tools:  make(map[string]Tool),
		config: config,
	}
}

// RegisterTool 注册一个工具到 Agent
func (a *ReActAgent) RegisterTool(t Tool) {
	a.tools[t.Name()] = t
}

// Run 执行完整的 ReAct 循环,处理用户请求
func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error) {
	// 初始化上下文:系统提示 + 用户输入
	messages := a.buildInitialMessages(userInput)

	for step := 0; step < a.config.MaxSteps; step++ {
		a.state = StateThinking

		// 1. 调用 LLM 生成 Thought + Action
		response, err := a.llm.Chat(ctx, messages)
		if err != nil {
			a.state = StateError
			return "", fmt.Errorf("LLM 调用失败(step %d): %w", step, err)
		}

		// 2. 解析 LLM 输出为 ReAct 格式
		reactStep, isFinal, err := a.parseResponse(response)
		if err != nil {
			a.state = StateError
			return "", fmt.Errorf("解析 LLM 输出失败(step %d): %w", step, err)
		}
		a.steps = append(a.steps, reactStep)

		if isFinal {
			a.state = StateFinished
			return reactStep.Input, nil // FINISH 的内容就是最终答案
		}

		// 3. 执行工具调用
		a.state = StateActing
		tool, ok := a.tools[reactStep.Action]
		if !ok {
			observation := fmt.Sprintf("错误: 未找到工具 '%s'。可用工具: %v",
				reactStep.Action, a.toolNames())
			messages = append(messages, assistantMsg(reactStep), userMsg(observation))
			continue
		}

		observation, err := tool.Execute(ctx, json.RawMessage(reactStep.Input))
		if err != nil {
			observation = fmt.Sprintf("工具执行错误: %v", err)
		}

		// 4. 追加 Observation 到对话历史
		a.state = StateObserving
		messages = append(messages,
			assistantMsg(reactStep),
			userMsg(fmt.Sprintf("Observation: %s", observation)),
		)

		if a.config.Verbose {
			fmt.Printf("[Step %d] Thought: %s\n  Action: %s(%s)\n  Observation: %s\n",
				step, reactStep.Thought, reactStep.Action, reactStep.Input, observation)
		}
	}

	return "", fmt.Errorf("超过最大步数限制 (%d)", a.config.MaxSteps)
}

// buildInitialMessages 构建初始 Prompt
//
// ReAct Prompt 模板设计是关键——这里通过 Markdown 层级结构
// 精准约束 LLM 的输出格式
func (a *ReActAgent) buildInitialMessages(userInput string) []Message {
	systemPrompt := fmt.Sprintf(`你是一个 AI 智能体,通过交替进行思考和行动来解决用户任务。

## 规则
1. 每轮输出必须严格遵循以下格式:
   Thought: <你的推理过程>
   Action: <工具名>(<JSON参数>)  或  Action: FINISH(<最终答案>)
2. 一次只能调用一个工具
3. 当任务完成时,使用 FINISH 给出最终答案
4. 如果工具执行失败,分析错误并尝试其他方法

## 可用工具
%s`, a.formatToolDescriptions())

	return []Message{
		{Role: "system", Content: systemPrompt},
		{Role: "user", Content: userInput},
	}
}

// parseResponse 解析 LLM 输出
func (a *ReActAgent) parseResponse(response string) (step ReActStep, isFinal bool, err error) {
	// 提取 Thought
	thoughtStart := strings.Index(response, "Thought:")
	actionStart := strings.Index(response, "Action:")

	if thoughtStart == -1 || actionStart == -1 {
		return step, false, fmt.Errorf("缺少 Thought: 或 Action: 字段:\n%s", response)
	}

	step.Thought = strings.TrimSpace(response[thoughtStart+9 : actionStart])
	actionPart := strings.TrimSpace(response[actionStart+8:])

	// 检查是否是 FINISH
	if strings.HasPrefix(actionPart, "FINISH(") && strings.HasSuffix(actionPart, ")") {
		finalAnswer := actionPart[7 : len(actionPart)-1]
		return ReActStep{
			Thought: step.Thought,
			Action:  "FINISH",
			Input:   finalAnswer,
		}, true, nil
	}

	// 解析工具调用: ToolName({"key": "value"})
	parenIdx := strings.Index(actionPart, "(")
	if parenIdx == -1 || !strings.HasSuffix(actionPart, ")") {
		return step, false, fmt.Errorf("工具调用格式错误(应为 ToolName({...})):\n%s", actionPart)
	}

	step.Action = actionPart[:parenIdx]
	step.Input = actionPart[parenIdx+1 : len(actionPart)-1]
	return step, false, nil
}

// 辅助方法

func (a *ReActAgent) formatToolDescriptions() string {
	var sb strings.Builder
	for _, t := range a.tools {
		sb.WriteString(fmt.Sprintf("- **%s**: %s\n", t.Name(), t.Description()))
		// 如果工具提供了 JSON Schema,一并展示
		if schema := t.JSONSchema(); len(schema) > 0 {
			sb.WriteString(fmt.Sprintf("  参数格式: ```json\n  %s\n  ```\n", string(schema)))
		}
	}
	return sb.String()
}

func (a *ReActAgent) toolNames() []string {
	names := make([]string, 0, len(a.tools))
	for name := range a.tools {
		names = append(names, name)
	}
	return names
}

// 消息辅助函数
func assistantMsg(s ReActStep) Message {
	return Message{Role: "assistant", Content: fmt.Sprintf(
		"Thought: %s\nAction: %s(%s)", s.Thought, s.Action, s.Input)}
}

func userMsg(content string) Message {
	return Message{Role: "user", Content: content}
}

// 类型定义

type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

// LLMClient 抽象 LLM 接口(实现可使用 OpenAI / Ollama / vLLM 等后端)
type LLMClient interface {
	Chat(ctx context.Context, messages []Message) (string, error)
}

使用 Python 实现 ReAct Agent ​

python
"""
ReAct Agent 的 Python 实现(使用 OpenAI 兼容接口)
"""
import json
import re
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional
from abc import ABC, abstractmethod

# --- 工具抽象 ---

class Tool(ABC):
    """工具基类,所有 Agent 工具需继承此类"""

    @property
    @abstractmethod
    def name(self) -> str:
        """工具名称"""
        ...

    @property
    @abstractmethod
    def description(self) -> str:
        """工具描述(会写入 Prompt 中)"""
        ...

    @property
    @abstractmethod
    def parameters(self) -> Dict[str, Any]:
        """JSON Schema 格式的参数定义"""
        ...

    @abstractmethod
    async def execute(self, **kwargs) -> str:
        """执行工具,返回字符串结果"""
        ...


# --- ReAct Agent 实现 ---

@dataclass
class ReActStep:
    """ReAct 循环中的一步"""
    thought: str   # 推理过程
    action: str    # 工具名或 "FINISH"
    input: str     # 工具参数(JSON 字符串)或最终答案


@dataclass
class AgentConfig:
    """Agent 配置"""
    max_steps: int = 10          # 最大步数
    temperature: float = 0.7     # 模型温度
    system_prompt: str = ""      # 系统提示词
    verbose: bool = True         # 是否打印日志


class ReActAgent:
    """
    基于 ReAct 范式的智能体

    工作原理:
    1. 接收用户任务
    2. 构建 Prompt(系统提示 + 工具列表 + 用户输入)
    3. 循环调用 LLM:
       - LLM 输出 Thought + Action
       - 解析后调用对应工具
       - 将 Observation 追加到上下文
    4. LLM 输出 FINISH 时结束循环
    """

    def __init__(
        self,
        llm_client: Any,  # OpenAI 兼容客户端
        config: AgentConfig = None,
    ):
        self.llm = llm_client
        self.config = config or AgentConfig()
        self.tools: Dict[str, Tool] = {}
        self.steps: List[ReActStep] = []

    def register_tool(self, tool: Tool):
        """注册工具"""
        self.tools[tool.name] = tool

    async def run(self, user_input: str) -> str:
        """执行 ReAct 循环"""
        # 构建初始消息
        messages = self._build_initial_messages(user_input)

        for step_num in range(self.config.max_steps):
            if self.config.verbose:
                print(f"\n{'='*50}")
                print(f"🔄 Step {step_num + 1}/{self.config.max_steps}")

            # 1. 调用 LLM
            response = await self._call_llm(messages)

            # 2. 解析输出
            react_step, is_final = self._parse_response(response)
            self.steps.append(react_step)

            if is_final:
                return react_step.input  # FINISH 的内容 = 最终答案

            # 3. 执行工具
            tool = self.tools.get(react_step.action)
            if tool is None:
                observation = f"错误: 未找到工具 '{react_step.action}'。可用工具: {list(self.tools.keys())}"
            else:
                try:
                    params = json.loads(react_step.input) if react_step.input else {}
                    observation = await tool.execute(**params)
                except json.JSONDecodeError:
                    observation = f"错误: 工具参数 JSON 解析失败: {react_step.input}"
                except Exception as e:
                    observation = f"工具执行异常: {type(e).__name__}: {e}"

            # 4. 追加到对话历史
            messages.append({
                "role": "assistant",
                "content": f"Thought: {react_step.thought}\nAction: {react_step.action}({react_step.input})"
            })
            messages.append({"role": "user", "content": f"Observation: {observation}"})

            if self.config.verbose:
                print(f"  💭 Thought: {react_step.thought[:100]}...")
                print(f"  🔧 Action: {react_step.action}({react_step.input[:100]}...)")
                print(f"  👁️  Observation: {observation[:100]}...")

        raise RuntimeError(f"Agent 超过最大步数限制 ({self.config.max_steps})")

    def _build_initial_messages(self, user_input: str) -> List[Dict]:
        """构建 ReAct Prompt —— 层次化 Markdown 模板"""
        tools_desc = ""
        for name, tool in self.tools.items():
            params_schema = json.dumps(tool.parameters, ensure_ascii=False, indent=2)
            tools_desc += f"- **{name}**: {tool.description}\n"
            tools_desc += f"  ```json\n  {params_schema}\n  ```\n"

        system_prompt = f"""你是一个 AI 智能体,通过交替进行思考和行动来解决用户任务。

## 输出格式(严格遵守)
每轮输出必须包含:

Thought: <你对当前状况的分析和下一步计划> Action: <工具名>(<JSON格式参数>) 或 Action: FINISH(<最终答案>)


## 规则
1. 一次只能调用一个工具
2. 必须在 Action 中给出合法的 JSON 参数
3. 使用 FINISH 报告最终结果
4. 如果出错,分析原因并调整策略

## 可用工具
{tools_desc}
"""
        return [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_input},
        ]

    def _parse_response(self, response: str) -> tuple:
        """解析 LLM 输出"""
        # 提取 Thought
        thought_match = re.search(r'Thought:\s*(.+?)(?=\nAction:|$)', response, re.DOTALL)
        action_match = re.search(r'Action:\s*(.+)$', response, re.MULTILINE)

        if not thought_match or not action_match:
            raise ValueError(f"无法解析 LLM 输出:\n{response}")

        thought = thought_match.group(1).strip()
        action_text = action_match.group(1).strip()

        # 检查 FINISH
        finish_match = re.match(r'FINISH\((.+)\)', action_text, re.DOTALL)
        if finish_match:
            return ReActStep(thought=thought, action="FINISH", input=finish_match.group(1)), True

        # 解析工具调用: ToolName({"key": "value"})
        tool_match = re.match(r'(\w+)\((.+)\)', action_text, re.DOTALL)
        if not tool_match:
            raise ValueError(f"无法解析工具调用格式: {action_text}")

        return ReActStep(
            thought=thought,
            action=tool_match.group(1),
            input=tool_match.group(2),
        ), False

    async def _call_llm(self, messages: List[Dict]) -> str:
        """调用 LLM(OpenAI 兼容接口)"""
        # 注意: 这里需要配合实际的 LLM 客户端实现
        # 例如 openai.AsyncOpenAI().chat.completions.create(...)
        raise NotImplementedError("需要实现具体的 LLM 调用逻辑")

严谨的 Function Calling 设计 ​

JSON Schema 工具定义 ​

将后端 API 抽象为标准的 JSON Schema,让 LLM 能精确生成调用参数。

python
"""
严谨的 Function Calling 工具定义示例

核心原则:
1. Schema 要精确(字段、类型、约束都明确)
2. Description 要清晰(告诉模型何时用、怎么用)
3. 必填项要明确标记
"""

GET_WEATHER_TOOL = {
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "获取指定城市的实时天气信息。当用户询问天气、气温、降雨等情况时使用此工具。",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名称,支持中英文(如:'深圳'、'Shenzhen')",
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "温度单位,默认为 celsius(摄氏度)",
                },
            },
            "required": ["city"],  # unit 是可选的,有默认值
        },
    },
}

EXECUTE_SQL_TOOL = {
    "type": "function",
    "function": {
        "name": "execute_sql",
        "description": (
            "在数据库上执行只读 SQL 查询。只支持 SELECT 语句。"
            "数据库包含以下表:users(id, name, email), orders(id, user_id, amount, created_at)"
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "sql": {
                    "type": "string",
                    "description": "待执行的 SELECT SQL 语句。必须是完整有效的 SQL。禁止 DDL/DML。",
                },
                "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "description": "最大返回行数,默认 20",
                },
            },
            "required": ["sql"],
        },
    },
}

JSON 容错与自动修复 ​

LLM 输出的 JSON 经常有瑕疵——多余的逗号、缺失引号、注释等。后端的自动修复机制至关重要:

go
package functioncalling

import (
	"encoding/json"
	"fmt"
	"regexp"
	"strings"
)

// RepairJSON 自动修复 LLM 输出的损坏 JSON
//
// 常见 LLM JSON 错误:
// 1. 尾部多余逗号: {"name": "test",}
// 2. 缺少引号的 key: {name: "test"}
// 3. 单引号: {'name': 'test'}
// 4. 注释混入: {"name": "test" /* comment */}
// 5. 多余文本包围: 结果是 {"name": "test"},请检查
// 6. 数组尾部多余逗号: [1, 2, 3,]
func RepairJSON(raw string) (string, error) {
	// Step 1: 提取 JSON 部分(去除 Markdown 代码块包裹)
	raw = extractJSONBlock(raw)

	// Step 2: 去除注释
	raw = removeComments(raw)

	// Step 3: 替换单引号为双引号(注意字符串内的单引号)
	raw = normalizeQuotes(raw)

	// Step 4: 修复无引号的 key
	raw = quoteUnquotedKeys(raw)

	// Step 5: 去除尾部多余逗号
	raw = removeTrailingCommas(raw)

	// Step 6: 验证修复结果
	var result interface{}
	if err := json.Unmarshal([]byte(raw), &result); err != nil {
		return raw, fmt.Errorf("JSON 修复后仍无效: %w\n修复结果: %s", err, raw)
	}

	return raw, nil
}

// extractJSONBlock 从 LLM 输出中提取 JSON
func extractJSONBlock(raw string) string {
	// 尝试匹配 ```json ... ``` 或 ``` ... ```
	re := regexp.MustCompile("(?s)```(?:json)?\\s*\\n?(.+?)\\n?```")
	if matches := re.FindStringSubmatch(raw); len(matches) > 1 {
		return strings.TrimSpace(matches[1])
	}

	// 尝试找到第一个 { 和最后一个 }
	start := strings.Index(raw, "{")
	end := strings.LastIndex(raw, "}")
	if start != -1 && end > start {
		return raw[start : end+1]
	}

	// 尝试找到第一个 [ 和最后一个 ]
	start = strings.Index(raw, "[")
	end = strings.LastIndex(raw, "]")
	if start != -1 && end > start {
		return raw[start : end+1]
	}

	return raw
}

// removeComments 去除 JSON 中的注释
func removeComments(raw string) string {
	// 去除行注释 //
	reLine := regexp.MustCompile(`//[^\n]*`)
	raw = reLine.ReplaceAllString(raw, "")

	// 去除块注释 /* */
	reBlock := regexp.MustCompile(`/\*[\s\S]*?\*/`)
	raw = reBlock.ReplaceAllString(raw, "")

	return raw
}

// normalizeQuotes 将单引号标准化为双引号
func normalizeQuotes(raw string) string {
	// 简化处理:先将所有单引号替换为双引号
	// 注意:这可能会破坏字符串内容中的单引号(如 "it's")
	// 生产环境需要更复杂的基于状态的解析
	result := strings.ReplaceAll(raw, `'`, `"`)
	return result
}

// quoteUnquotedKeys 为无引号的 JSON key 添加引号
func quoteUnquotedKeys(raw string) string {
	// 匹配 { 或 , 后的无引号标识符:
	// 如 {name: "test"} → {"name": "test"}
	re := regexp.MustCompile(`([{,])\s*([a-zA-Z_]\w*)\s*:`)
	return re.ReplaceAllString(raw, `$1"$2":`)
}

// removeTrailingCommas 去除尾部多余逗号
func removeTrailingCommas(raw string) string {
	// 对象尾部: {"a": 1,} → {"a": 1}
	reObj := regexp.MustCompile(`,(\s*[}\]])`)
	raw = reObj.ReplaceAllString(raw, `$1`)

	// 数组尾部: [1, 2,] → [1, 2]
	return raw
}

// SafeCallTool 带容错的工具调用封装
//
// 当 LLM 输出损坏的 JSON 时,自动尝试最多 3 次修复与重试
func SafeCallTool(
	toolName string,
	rawArgs string,
	execute func(args json.RawMessage) (string, error),
) (result string, err error) {
	// 第一次尝试:直接解析
	args, err := RepairJSON(rawArgs)
	if err == nil {
		result, callErr := execute(json.RawMessage(args))
		if callErr == nil {
			return result, nil
		}
		// 工具执行错误直接返回(不是 JSON 问题)
		return "", fmt.Errorf("工具 %s 执行失败: %w", toolName, callErr)
	}

	// JSON 解析失败,返回详细的错误信息给 LLM,让它重新生成
	return "", fmt.Errorf(
		"工具 %s 的 JSON 参数无效。原始输入: %s\n错误: %w\n"+
			"请修正 JSON 格式后重试。"+
			"注意: key 必须用双引号包裹,字符串值必须用双引号,不能使用尾随逗号。",
		toolName, rawArgs, err,
	)
}

容错重试的完整流程 ​

mermaid
graph TD
    LLM["LLM 输出 Tool Call"] --> PARSE{"JSON 解析成功?"}
    PARSE -->|是| EXEC["执行工具"]
    PARSE -->|否| FIX["自动修复 JSON<br/>(去注释、补引号、去尾部逗号)"]
    FIX --> RETRY1{"第2次解析成功?"}
    RETRY1 -->|是| EXEC
    RETRY1 -->|否| ERR["返回详细错误给 LLM<br/>提示 JSON 格式要求"]
    ERR --> LLM2["LLM 重新生成"]
    LLM2 --> PARSE2{"第3次解析成功?"}
    PARSE2 -->|是| EXEC
    PARSE2 -->|否| FALLBACK["最终降级:<br/>返回错误并提示用户"]

    style PARSE fill:#f39c12,color:#fff
    style FIX fill:#2ecc71,color:#fff
    style FALLBACK fill:#e74c3c,color:#fff

状态管理与多轮对话持久化 ​

会话生命周期 ​

mermaid
stateDiagram-v2
    [*] --> Created: 用户打开界面
    Created --> Active: 发送第一条消息
    Active --> Active: 多轮对话
    Active --> Paused: 用户离开(超时)
    Paused --> Active: 用户返回
    Active --> Expired: 超时未活动(>30min)
    Active --> Closed: 用户关闭
    Expired --> [*]
    Closed --> [*]

使用 Redis 管理 Session ​

go
package session

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

// Session 表示一个用户的对话会话
type Session struct {
	ID        string    `json:"id"`        // Session ID (UUID)
	UserID    string    `json:"user_id"`   // 用户标识
	Messages  []Message `json:"messages"`  // 对话历史(最近 N 轮)
	Summary   string    `json:"summary"`   // 早期对话的摘要(压缩长历史)
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
	TTL       time.Duration `json:"-"`     // 过期时间
}

// SessionManager 使用 Redis 管理对话会话
type SessionManager struct {
	rdb        *redis.Client
	maxHistory int           // 最大保留的历史消息数(超过则压缩)
	defaultTTL time.Duration // 默认 Session 过期时间
}

func NewSessionManager(rdb *redis.Client, maxHistory int) *SessionManager {
	return &SessionManager{
		rdb:        rdb,
		maxHistory: maxHistory,
		defaultTTL: 30 * time.Minute, // 30 分钟无活动则过期
	}
}

// CreateSession 创建新的对话会话
func (sm *SessionManager) CreateSession(ctx context.Context, userID string) (*Session, error) {
	sessionID := generateUUID() // 使用 UUID 生成 session ID
	session := &Session{
		ID:        sessionID,
		UserID:    userID,
		Messages:  []Message{},
		CreatedAt: time.Now(),
		UpdatedAt: time.Now(),
		TTL:       sm.defaultTTL,
	}

	return session, sm.saveSession(ctx, session)
}

// AddMessage 向会话中追加一条消息
func (sm *SessionManager) AddMessage(ctx context.Context, sessionID string, msg Message) error {
	session, err := sm.GetSession(ctx, sessionID)
	if err != nil {
		return err
	}

	session.Messages = append(session.Messages, msg)

	// 如果消息过多,压缩早期对话为摘要
	if len(session.Messages) > sm.maxHistory {
		session.Messages, session.Summary = sm.compress(session)
	}

	session.UpdatedAt = time.Now()
	return sm.saveSession(ctx, session)
}

// GetSession 获取会话
func (sm *SessionManager) GetSession(ctx context.Context, sessionID string) (*Session, error) {
	data, err := sm.rdb.Get(ctx, sessionKey(sessionID)).Bytes()
	if err == redis.Nil {
		return nil, fmt.Errorf("session %s 不存在或已过期", sessionID)
	}
	if err != nil {
		return nil, fmt.Errorf("读取 session 失败: %w", err)
	}

	var session Session
	if err := json.Unmarshal(data, &session); err != nil {
		return nil, fmt.Errorf("解析 session 失败: %w", err)
	}
	return &session, nil
}

// saveSession 持久化会话到 Redis
func (sm *SessionManager) saveSession(ctx context.Context, s *Session) error {
	data, err := json.Marshal(s)
	if err != nil {
		return err
	}
	return sm.rdb.Set(ctx, sessionKey(s.ID), data, s.TTL).Err()
}

// compress 压缩历史对话:保留最近 N 条,将更早的压缩为摘要
func (sm *SessionManager) compress(s *Session) (recent []Message, summary string) {
	keepCount := sm.maxHistory / 2
	oldMessages := s.Messages[:len(s.Messages)-keepCount]
	recent = s.Messages[len(s.Messages)-keepCount:]

	// 将旧消息拼接为文本
	var oldContent string
	for _, m := range oldMessages {
		oldContent += fmt.Sprintf("[%s]: %s\n", m.Role, m.Content)
	}

	// 合并已有摘要和新摘要
	if s.Summary != "" {
		summary = fmt.Sprintf("之前对话摘要:\n%s\n\n最近对话:\n%s", s.Summary, oldContent)
	} else {
		summary = fmt.Sprintf("对话历史摘要:\n%s", oldContent)
	}

	return recent, summary
}

// Lua 脚本:原子性地更新 Session(防止并发写冲突)
const luaUpdateSession = `
	local key = KEYS[1]
	local msg_json = ARGV[1]
	local max_history = tonumber(ARGV[2])

	local data = redis.call('GET', key)
	if not data then return nil end

	local session = cjson.decode(data)
	local msg = cjson.decode(msg_json)

	-- 追加消息
	table.insert(session.messages, msg)

	-- 如果消息过多,截断(Lua 中简化处理)
	if #session.messages > max_history then
		local keep = math.floor(max_history / 2)
		local old = {}
		for i = 1, #session.messages - keep do
			table.insert(old, session.messages[i])
		end
		local recent = {}
		for i = #session.messages - keep + 1, #session.messages do
			table.insert(recent, session.messages[i])
		end
		session.messages = recent
		session.summary = '历史对话已压缩'
	end

	session.updated_at = ARGV[3]
	redis.call('SET', key, cjson.encode(session), 'EX', ARGV[4])
	return 1
`

func (sm *SessionManager) AddMessageAtomic(ctx context.Context, sessionID string, msg Message) error {
	return sm.rdb.Eval(ctx, luaUpdateSession,
		[]string{sessionKey(sessionID)},
		mustMarshal(msg), sm.maxHistory, time.Now().Format(time.RFC3339), int(sm.defaultTTL.Seconds()),
	).Err()
}

func sessionKey(id string) string {
	return fmt.Sprintf("agent:session:%s", id)
}

// 简化:实际项目应使用 UUID 库
func generateUUID() string {
	return fmt.Sprintf("%d-%d", time.Now().UnixNano(), time.Now().Nanosecond())
}

func mustMarshal(v interface{}) string {
	data, _ := json.Marshal(v)
	return string(data)
}

使用 MySQL 存储 Session(备选方案) ​

sql
-- Agent 会话持久化表
CREATE TABLE agent_sessions (
    id          VARCHAR(36) PRIMARY KEY,          -- UUID
    user_id     VARCHAR(64) NOT NULL,              -- 用户标识
    messages    JSON NOT NULL DEFAULT ('[]'),      -- 对话历史 [{role, content, time}]
    summary     TEXT DEFAULT '',                   -- 历史对话摘要
    metadata    JSON DEFAULT ('{}'),               -- 扩展元数据(模型、参数等)
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_user_id (user_id),
    INDEX idx_updated_at (updated_at)              -- 用于清理过期会话
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 定期清理过期会话的定时任务
-- DELETE FROM agent_sessions WHERE updated_at < NOW() - INTERVAL 1 DAY;

完整 Agent 架构图 ​

mermaid
graph TD
    subgraph "前端"
        UI["HTML+JS Web 界面<br/>对话窗口 + 工具展示"]
    end

    subgraph "API 网关"
        GW["WebSocket / HTTP<br/>流式传输 SSE"]
    end

    subgraph "Agent 服务"
        ROUTER["请求路由<br/>按 User/Session 分发"]
        AGT["ReAct Agent 引擎<br/>思考→行动→观察循环"]
        FCP["Function Calling<br/>JSON Schema → 工具执行"]
        SM["Session Manager<br/>Redis/MySQL 多轮对话"]
    end

    subgraph "LLM 后端"
        LLM1["OpenAI API"]
        LLM2["本地 llama.cpp<br/>Metal / CUDA"]
        LLM3["vLLM / TGI"]
    end

    subgraph "工具集"
        T1["天气 API"]
        T2["SQL 查询"]
        T3["代码执行"]
        T4["文件操作"]
    end

    UI <--> GW
    GW <--> ROUTER
    ROUTER --> AGT
    AGT --> FCP
    AGT --> SM
    AGT --> LLM1
    AGT --> LLM2
    AGT --> LLM3
    FCP --> T1
    FCP --> T2
    FCP --> T3
    FCP --> T4

    style AGT fill:#9b59b6,color:#fff
    style FCP fill:#2ecc71,color:#fff
    style SM fill:#3498db,color:#fff

核心设计原则:Agent 引擎(ReAct 循环)负责调度推理和工具调用,Session Manager 负责持久化上下文,Function Calling 负责将自然语言指令转化为精确的 API 调用。三者解耦,各自演进。


LangGraph — 图结构的 Agent 编排 ​

为什么需要 LangGraph? ​

ReAct 是线性循环,但真实的 Agent 任务往往需要分支、并行、条件跳转。LangGraph 将 Agent 的执行流程建模为有向图:

mermaid
graph TD
    START["开始"] --> CLASSIFY["分类器<br/>判断任务类型"]
    CLASSIFY -->|"代码问题"| CODE["代码 Agent<br/>搜索代码库+生成"]
    CLASSIFY -->|"文档问题"| DOC["文档 Agent<br/>RAG 检索+总结"]
    CLASSIFY -->|"数据问题"| DATA["数据 Agent<br/>SQL 查询+可视化"]

    CODE --> REVIEW["审查节点<br/>检查代码质量"]
    DOC --> FORMAT["格式化节点<br/>Markdown 输出"]
    DATA --> FORMAT

    REVIEW -->|"通过"| END["返回结果"]
    REVIEW -->|"不通过"| CODE
    FORMAT --> END

    style CLASSIFY fill:#f39c12,color:#fff
    style REVIEW fill:#e74c3c,color:#fff

LangGraph 核心概念 ​

概念说明类比
State图的全局状态(贯穿所有节点)全局变量
Node图中的一个处理节点(函数)函数
Edge节点之间的连接函数调用
Conditional Edge根据状态决定下一个节点if-else
Checkpoint状态快照(支持回溯和恢复)存档点

Python 实现示例 ​

python
"""
LangGraph 风格的 Agent 图实现

核心思想:将 Agent 的执行流程建模为状态机图
"""

from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, TypedDict
from enum import Enum
import asyncio


class NodeStatus(Enum):
    """节点执行状态"""
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"


class AgentState(TypedDict, total=False):
    """Agent 全局状态(贯穿所有节点)"""
    messages: List[dict]       # 对话历史
    task_type: str             # 任务分类
    intermediate_results: List[str]  # 中间结果
    final_answer: str          # 最终答案
    error: Optional[str]       # 错误信息
    retry_count: int           # 重试次数
    tools_used: List[str]      # 已使用的工具


class StateGraph:
    """
    状态图 — LangGraph 的核心抽象

    用法:
    1. 定义节点(处理函数)
    2. 定义边(节点间的连接)
    3. 编译并运行
    """

    def __init__(self, state_class: type):
        self.state_class = state_class
        self.nodes: Dict[str, Callable] = {}
        self.edges: Dict[str, List[str]] = {}
        self.conditional_edges: Dict[str, Callable] = {}
        self.entry_point: str = ""

    def add_node(self, name: str, func: Callable):
        """添加节点"""
        self.nodes[name] = func
        if not self.entry_point:
            self.entry_point = name

    def add_edge(self, from_node: str, to_node: str):
        """添加固定边"""
        if from_node not in self.edges:
            self.edges[from_node] = []
        self.edges[from_node].append(to_node)

    def add_conditional_edge(self, from_node: str, condition_fn: Callable):
        """
        添加条件边

        condition_fn(state) -> str: 返回下一个节点的名称
        """
        self.conditional_edges[from_node] = condition_fn

    def set_entry_point(self, name: str):
        """设置入口节点"""
        self.entry_point = name

    async def run(self, initial_state: dict) -> dict:
        """执行图"""
        state = dict(initial_state)
        current_node = self.entry_point

        max_steps = 20  # 防止无限循环
        for step in range(max_steps):
            if current_node == "END" or current_node is None:
                break

            # 执行当前节点
            node_fn = self.nodes.get(current_node)
            if node_fn is None:
                raise ValueError(f"节点 '{current_node}' 不存在")

            # 节点函数接收 state,返回更新后的 state
            state = await node_fn(state) if asyncio.iscoroutinefunction(node_fn) else node_fn(state)

            # 决定下一个节点
            if current_node in self.conditional_edges:
                # 条件边:根据状态决定
                current_node = self.conditional_edges[current_node](state)
            elif current_node in self.edges:
                # 固定边:取第一个(简化)
                current_node = self.edges[current_node][0]
            else:
                current_node = "END"

        return state


# ========== 构建一个多功能 Agent 图 ==========

async def classify_task(state: dict) -> dict:
    """分类节点:判断用户任务类型"""
    messages = state.get("messages", [])
    last_msg = messages[-1]["content"] if messages else ""

    # 简单的关键词分类(实际用 LLM)
    if any(kw in last_msg for kw in ["代码", "函数", "bug", "实现"]):
        state["task_type"] = "code"
    elif any(kw in last_msg for kw in ["文档", "解释", "什么是"]):
        state["task_type"] = "doc"
    elif any(kw in last_msg for kw in ["数据", "统计", "查询", "SQL"]):
        state["task_type"] = "data"
    else:
        state["task_type"] = "general"

    return state


async def code_agent(state: dict) -> dict:
    """代码 Agent 节点"""
    # 搜索代码库 → 生成代码 → 返回
    state["intermediate_results"] = state.get("intermediate_results", [])
    state["intermediate_results"].append("代码搜索完成")
    state["tools_used"] = state.get("tools_used", []) + ["code_search", "code_gen"]
    state["final_answer"] = "生成的代码..."
    return state


async def doc_agent(state: dict) -> dict:
    """文档 Agent 节点"""
    state["intermediate_results"] = state.get("intermediate_results", [])
    state["intermediate_results"].append("文档检索完成")
    state["tools_used"] = state.get("tools_used", []) + ["rag_search"]
    state["final_answer"] = "文档回答..."
    return state


async def data_agent(state: dict) -> dict:
    """数据 Agent 节点"""
    state["intermediate_results"] = state.get("intermediate_results", [])
    state["intermediate_results"].append("数据查询完成")
    state["tools_used"] = state.get("tools_used", []) + ["sql_query"]
    state["final_answer"] = "数据分析结果..."
    return state


async def review_node(state: dict) -> dict:
    """审查节点:检查结果质量"""
    # 简化:实际会用 LLM 评估
    state["retry_count"] = state.get("retry_count", 0)
    # 假设第一次通过
    return state


def route_by_task_type(state: dict) -> str:
    """条件路由:根据任务类型选择 Agent"""
    task_type = state.get("task_type", "general")
    return {
        "code": "code_agent",
        "doc": "doc_agent",
        "data": "data_agent",
        "general": "doc_agent",
    }.get(task_type, "doc_agent")


def route_after_review(state: dict) -> str:
    """审查后路由:通过则结束,否则重试"""
    if state.get("retry_count", 0) >= 2:
        return "END"  # 重试次数用完
    # 简化:总是通过
    return "END"


# 构建图
graph = StateGraph(AgentState)
graph.add_node("classify", classify_task)
graph.add_node("code_agent", code_agent)
graph.add_node("doc_agent", doc_agent)
graph.add_node("data_agent", data_agent)
graph.add_node("review", review_node)

graph.set_entry_point("classify")
graph.add_conditional_edge("classify", route_by_task_type)
graph.add_edge("code_agent", "review")
graph.add_edge("doc_agent", "review")
graph.add_edge("data_agent", "review")
graph.add_conditional_edge("review", route_after_review)

# 运行
# result = asyncio.run(graph.run({
#     "messages": [{"role": "user", "content": "帮我写一个排序函数"}],
# }))

Agent 编排进阶 — 生产级工作流模式 ​

LangGraph 提供了图编排的基础,但生产环境还需要更多编排模式:

Human-in-the-Loop — 人工审批节点 ​

python
"""
Human-in-the-Loop (HITL) — 关键决策需要人工审批

场景:
- 删除生产数据前需要人工确认
- 发布代码前需要 Code Review
- 成本超过预算时暂停执行
"""

class HITLNode:
    """
    人工审批节点

    流程:
    LLM 执行 → 到达审批节点 → 暂停 → 发通知 → 等待人工 → 继续/拒绝
    """

    def __init__(self, notifier=None):
        self.notifier = notifier  # 通知渠道(企业微信/邮件/Slack)
        self.pending_approvals: Dict[str, dict] = {}

    async def request_approval(self, task_id: str, context: dict,
                                timeout: int = 3600) -> bool:
        """
        请求人工审批

        Args:
            task_id: 任务 ID
            context: 审批上下文(做了什么、为什么需要审批)
            timeout: 超时时间(秒),超时自动拒绝

        Returns:
            approved: 是否通过
        """
        # 1. 保存审批请求
        approval_id = f"approval_{task_id}_{int(time.time())}"
        self.pending_approvals[approval_id] = {
            "task_id": task_id,
            "context": context,
            "status": "pending",
            "created_at": time.time(),
            "timeout": timeout,
        }

        # 2. 发送通知
        await self.notifier.send({
            "title": f"Agent 任务需要审批 — {task_id}",
            "summary": context.get("summary", "Agent 请求审批"),
            "details": json.dumps(context, ensure_ascii=False, indent=2),
            "actions": [
                {"label": "批准", "value": "approve", "style": "primary"},
                {"label": "拒绝", "value": "reject", "style": "danger"},
            ],
            "approval_id": approval_id,
        })

        # 3. 等待审批结果(轮询或 WebSocket)
        deadline = time.time() + timeout
        while time.time() < deadline:
            approval = self.pending_approvals.get(approval_id)
            if approval and approval["status"] != "pending":
                return approval["status"] == "approved"
            await asyncio.sleep(2)

        # 超时
        self.pending_approvals[approval_id]["status"] = "timeout"
        return False

    def handle_webhook(self, approval_id: str, action: str):
        """处理审批回调"""
        if approval_id in self.pending_approvals:
            self.pending_approvals[approval_id]["status"] = (
                "approved" if action == "approve" else "rejected"
            )


# 在 Agent 图中添加审批节点
class AgentWithApproval:
    """带审批的 Agent"""

    def __init__(self):
        self.hitl = HITLNode()
        self.graph = StateGraph(AgentState)

    async def approval_node(self, state: dict) -> dict:
        """审批节点:检查是否需要人工审批"""
        task_type = state.get("task_type", "")
        action = state.get("planned_action", "")

        # 危险操作需要审批
        dangerous_actions = ["删除", "发布", "修改生产配置", "执行 SQL"]
        needs_approval = any(a in action for a in dangerous_actions)

        if needs_approval:
            approved = await self.hitl.request_approval(
                task_id=state.get("task_id", ""),
                context={
                    "summary": f"Agent 计划执行: {action}",
                    "risk": "此操作可能影响生产环境",
                    "alternatives": "可以拒绝并手动执行",
                },
            )
            state["approved"] = approved
            if not approved:
                state["error"] = "操作被人工拒绝"

        return state


# Graph 示例
# graph.add_node("approval_check", approval_node)
# graph.add_conditional_edge("approval_check", lambda s: "execute" if s.get("approved") else "END")

工作流编排模式对比 ​

模式框架特点适用场景复杂度
DAG (有向无环图)LangGraph / Airflow确定性的任务依赖数据流水线、CI/CD⭐⭐
状态机自研 / XState有限状态 + 事件驱动审批流程、订单状态⭐⭐
动态规划LangGraph + LLMLLM 动态决定下一步复杂 Agent 任务⭐⭐⭐
工作流引擎Temporal / Prefect持久化、重试、超时长时间运行的任务⭐⭐⭐
发布-订阅消息队列 + Agent事件驱动、松耦合微服务、异步处理⭐⭐⭐

并行 Agent 编排 ​

python
"""
并行 Agent 编排 — 多个 Agent 同时工作,结果合并

场景:
- 同时搜索多个数据源,合并结果
- 同时生成多个候选方案,投票选最佳
- 代码 + 文档同时生成
"""

import asyncio
from typing import List


class ParallelOrchestrator:
    """并行编排器"""

    async def run_parallel(self, tasks: List[dict], merge_strategy: str = "concat") -> dict:
        """
        并行执行多个 Agent 任务

        Args:
            tasks: 任务列表 [{"agent": agent_fn, "input": ...}]
            merge_strategy: 合并策略
                - concat: 拼接结果
                - vote: 投票选最佳
                - first: 返回第一个完成的结果(race)
        """
        # 并发启动所有 Agent
        async def run_task(task):
            try:
                agent_fn = task["agent"]
                result = await agent_fn(task["input"])
                return {"success": True, "result": result, "task": task}
            except Exception as e:
                return {"success": False, "error": str(e), "task": task}

        coroutines = [run_task(t) for t in tasks]

        if merge_strategy == "first":
            # Race:返回第一个完成的结果
            for coro in asyncio.as_completed(coroutines):
                result = await coro
                if result["success"]:
                    # 取消其他任务
                    return {"winner": result, "strategy": "first"}

        # 等待全部完成
        results = await asyncio.gather(*coroutines, return_exceptions=True)

        successful = [r for r in results if not isinstance(r, Exception) and r["success"]]

        if merge_strategy == "concat":
            # 拼接所有结果
            combined = "\n\n---\n\n".join(
                r["result"] for r in successful
            )
            return {"combined": combined, "results": successful, "strategy": "concat"}

        elif merge_strategy == "vote":
            # 投票选最佳(需要 LLM-as-Judge)
            return await self._vote_best(successful)

    async def _vote_best(self, results: List[dict]) -> dict:
        """LLM 投票选出最佳结果"""
        # 用 Judge LLM 评估每个结果,选最优
        candidates = "\n---\n".join(
            f"候选 {i}: {r['result'][:200]}"
            for i, r in enumerate(results)
        )
        # judge_prompt = f"评估以下候选方案,选择最佳的一个:\n{candidates}"
        # winner_idx = await llm.judge(judge_prompt)
        return {"results": results, "winner_idx": 0, "strategy": "vote"}


# ========== 混合编排:串行 + 并行 ==========

async def hybrid_orchestrate(user_request: str) -> dict:
    """
    混合编排示例:串行步骤中包含并行子任务

    1. [串行] 分析需求 → 拆解为子任务
    2. [并行] 代码生成 + 文档生成 + 测试生成
    3. [串行] 整合结果 → 质量审查 → 返回
    """
    orchestrator = ParallelOrchestrator()

    # Step 1: 分析需求(串行)
    analysis = await analyze_requirement(user_request)

    # Step 2: 并行执行
    parallel_result = await orchestrator.run_parallel([
        {"agent": generate_code, "input": analysis},
        {"agent": generate_docs, "input": analysis},
        {"agent": generate_tests, "input": analysis},
    ], merge_strategy="concat")

    # Step 3: 审查结果(串行)
    final = await review_and_merge(parallel_result)

    return final

Agent 超时与重试策略 ​

python
"""
Agent 超时与重试 — 生产环境必备
"""

class AgentExecutor:
    """带超时和重试的 Agent 执行器"""

    def __init__(self, max_retries: int = 3, timeout: int = 60):
        self.max_retries = max_retries
        self.timeout = timeout

    async def execute_with_retry(self, agent_fn, input_data: dict,
                                  retryable_errors: tuple = (TimeoutError, ConnectionError)):
        """带重试的执行"""
        last_error = None

        for attempt in range(self.max_retries):
            try:
                # 带超时的执行
                result = await asyncio.wait_for(
                    agent_fn(input_data),
                    timeout=self.timeout,
                )
                return {"success": True, "result": result, "attempts": attempt + 1}

            except retryable_errors as e:
                last_error = e
                wait_time = min(2 ** attempt, 30)  # 指数退避,最多 30s
                print(f"重试 {attempt + 1}/{self.max_retries}," +
                      f"等待 {wait_time}s: {type(e).__name__}")
                await asyncio.sleep(wait_time)

            except Exception as e:
                # 不可重试的错误直接抛出
                return {"success": False, "error": str(e), "attempts": attempt + 1}

        return {
            "success": False,
            "error": f"重试 {self.max_retries} 次后仍失败: {last_error}",
            "attempts": self.max_retries,
        }


# 使用
executor = AgentExecutor(max_retries=3, timeout=60)
# result = await executor.execute_with_retry(my_agent, {"query": "..."})

Multi-Agent 系统 — 多个 Agent 协作 ​

为什么需要多 Agent? ​

单个 Agent 的能力有限。复杂任务需要多个专业 Agent 协作:

mermaid
graph TD
    subgraph "Multi-Agent 架构"
        USER["用户请求"] --> ORCHESTRATOR["协调者 Agent<br/>任务分解 + 调度"]

        ORCHESTRATOR --> CODER["编码 Agent<br/>写代码"]
        ORCHESTRATOR --> REVIEWER["审查 Agent<br/>代码审查"]
        ORCHESTRATOR --> TESTER["测试 Agent<br/>写测试"]
        ORCHESTRATOR --> RESEARCHER["研究 Agent<br/>搜索资料"]

        CODER --> ORCHESTRATOR
        REVIEWER --> ORCHESTRATOR
        TESTER --> ORCHESTRATOR
        RESEARCHER --> ORCHESTRATOR

        ORCHESTRATOR --> RESULT["最终结果"]
    end

    style ORCHESTRATOR fill:#9b59b6,color:#fff
    style CODER fill:#3498db,color:#fff
    style REVIEWER fill:#e74c3c,color:#fff
    style TESTER fill:#2ecc71,color:#fff

Multi-Agent 通信模式 ​

模式说明适用场景
中心化一个协调者分配任务任务明确、流程固定
去中心化Agent 之间直接通信需要灵活协作
层级式多层管理者 + 执行者大型复杂任务
辩论式多个 Agent 讨论达成共识需要多角度分析
python
"""
Multi-Agent 协作框架
"""

from dataclasses import dataclass
from typing import Dict, List, Optional
from abc import ABC, abstractmethod


@dataclass
class AgentMessage:
    """Agent 间的消息"""
    sender: str       # 发送者 Agent 名称
    receiver: str     # 接收者 Agent 名称("all" 表示广播)
    content: str      # 消息内容
    msg_type: str     # 消息类型: task/result/feedback/question


class BaseAgent(ABC):
    """Agent 基类"""

    def __init__(self, name: str, role: str, llm_client=None):
        self.name = name
        self.role = role
        self.llm = llm_client
        self.inbox: List[AgentMessage] = []

    @abstractmethod
    async def process(self, task: str) -> str:
        """处理任务"""
        ...

    async def receive(self, message: AgentMessage):
        """接收消息"""
        self.inbox.append(message)

    def send(self, receiver: str, content: str, msg_type: str = "result") -> AgentMessage:
        """发送消息"""
        return AgentMessage(
            sender=self.name,
            receiver=receiver,
            content=content,
            msg_type=msg_type,
        )


class OrchestratorAgent(BaseAgent):
    """协调者 Agent — 负责任务分解和调度"""

    def __init__(self, llm_client=None):
        super().__init__("orchestrator", "任务协调者", llm_client)
        self.workers: Dict[str, BaseAgent] = {}

    def register_worker(self, agent: BaseAgent):
        """注册工作 Agent"""
        self.workers[agent.name] = agent

    async def process(self, task: str) -> str:
        """分解任务并协调执行"""
        # 1. 任务分解
        subtasks = await self._decompose_task(task)

        # 2. 分配给对应的 Agent
        results = {}
        for subtask in subtasks:
            agent_name = subtask["agent"]
            if agent_name in self.workers:
                result = await self.workers[agent_name].process(subtask["task"])
                results[agent_name] = result

        # 3. 汇总结果
        return await self._synthesize_results(task, results)

    async def _decompose_task(self, task: str) -> List[dict]:
        """使用 LLM 分解任务"""
        # 实际实现会调用 LLM
        return [
            {"agent": "coder", "task": f"实现: {task}"},
            {"agent": "tester", "task": f"为以下功能写测试: {task}"},
        ]

    async def _synthesize_results(self, task: str, results: Dict[str, str]) -> str:
        """汇总所有 Agent 的结果"""
        return "\n".join(f"[{k}]: {v}" for k, v in results.items())


class CoderAgent(BaseAgent):
    """编码 Agent"""

    def __init__(self, llm_client=None):
        super().__init__("coder", "资深开发工程师", llm_client)

    async def process(self, task: str) -> str:
        # 实际会调用 LLM 生成代码
        return f"代码实现: {task}"


class ReviewerAgent(BaseAgent):
    """审查 Agent"""

    def __init__(self, llm_client=None):
        super().__init__("reviewer", "代码审查专家", llm_client)

    async def process(self, task: str) -> str:
        return f"审查意见: {task}"


class TesterAgent(BaseAgent):
    """测试 Agent"""

    def __init__(self, llm_client=None):
        super().__init__("tester", "测试工程师", llm_client)

    async def process(self, task: str) -> str:
        return f"测试用例: {task}"


# ========== 辩论式 Multi-Agent ==========

class DebateSystem:
    """
    辩论式 Multi-Agent

    多个 Agent 对同一问题给出不同观点,通过辩论达成共识。
    适用于需要多角度分析的场景(如架构设计决策)。
    """

    def __init__(self, agents: List[BaseAgent], moderator: BaseAgent,
                 max_rounds: int = 3):
        self.agents = agents
        self.moderator = moderator
        self.max_rounds = max_rounds

    async def debate(self, topic: str) -> str:
        """进行辩论"""
        history = []

        for round_num in range(self.max_rounds):
            round_opinions = []

            # 每个 Agent 发表观点
            for agent in self.agents:
                context = f"话题: {topic}\n历史讨论:\n" + "\n".join(history)
                opinion = await agent.process(context)
                round_opinions.append(f"[{agent.name}]: {opinion}")
                history.append(f"第{round_num+1}轮 [{agent.name}]: {opinion}")

            # 主持人判断是否达成共识
            consensus = await self.moderator.process(
                f"判断以下讨论是否达成共识:\n" + "\n".join(round_opinions)
            )

            if "共识" in consensus or "一致" in consensus:
                return consensus

        # 未达成共识,由主持人总结
        return await self.moderator.process(
            f"讨论未达成共识,请总结各方观点并给出建议:\n" + "\n".join(history)
        )

MCP (Model Context Protocol) — 工具标准化协议 ​

MCP 在 Agent 中的角色 ​

MCP 解决了 Agent 工具调用的标准化问题。不同的 AI 客户端(Cursor、Claude Desktop、自定义 Agent)可以通过统一的 MCP 协议连接同一套工具:

mermaid
graph TD
    subgraph "AI 客户端"
        C1["Cursor"]
        C2["Claude Desktop"]
        C3["自定义 Agent"]
    end

    subgraph "MCP 协议层"
        PROTO["JSON-RPC over stdio/SSE<br/>标准化的工具发现+调用"]
    end

    subgraph "MCP Servers (工具)"
        S1["Git Server<br/>代码版本管理"]
        S2["Database Server<br/>数据库查询"]
        S3["Wiki Server<br/>知识库检索"]
        S4["CI/CD Server<br/>构建部署"]
    end

    C1 <--> PROTO
    C2 <--> PROTO
    C3 <--> PROTO
    PROTO <--> S1
    PROTO <--> S2
    PROTO <--> S3
    PROTO <--> S4

    style PROTO fill:#f39c12,color:#fff

MCP 协议核心 ​

能力说明示例
Tools可调用的工具函数搜索代码、执行 SQL、发送消息
Resources可读取的数据资源文件内容、数据库表结构、API 文档
Prompts预定义的提示模板代码审查模板、日志分析模板
Sampling请求 LLM 生成内容Server 主动请求 AI 分析

实现 MCP Server(Go 版本) ​

go
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
)

// MCP JSON-RPC 消息格式
type JSONRPCRequest struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      interface{}     `json:"id"`
	Method  string          `json:"method"`
	Params  json.RawMessage `json:"params,omitempty"`
}

type JSONRPCResponse struct {
	JSONRPC string      `json:"jsonrpc"`
	ID      interface{} `json:"id"`
	Result  interface{} `json:"result,omitempty"`
	Error   *RPCError   `json:"error,omitempty"`
}

type RPCError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

// Tool 定义
type ToolDefinition struct {
	Name        string      `json:"name"`
	Description string      `json:"description"`
	InputSchema interface{} `json:"inputSchema"`
}

// MCP Server
type MCPServer struct {
	tools map[string]ToolHandler
}

type ToolHandler func(args json.RawMessage) (string, error)

func NewMCPServer() *MCPServer {
	return &MCPServer{
		tools: make(map[string]ToolHandler),
	}
}

func (s *MCPServer) RegisterTool(name, description string, handler ToolHandler) {
	s.tools[name] = handler
}

func (s *MCPServer) Run() {
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line := scanner.Text()
		var req JSONRPCRequest
		if err := json.Unmarshal([]byte(line), &req); err != nil {
			continue
		}

		resp := s.handleRequest(req)
		data, _ := json.Marshal(resp)
		fmt.Println(string(data))
	}
}

func (s *MCPServer) handleRequest(req JSONRPCRequest) JSONRPCResponse {
	switch req.Method {
	case "initialize":
		return JSONRPCResponse{
			JSONRPC: "2.0",
			ID:      req.ID,
			Result: map[string]interface{}{
				"protocolVersion": "2024-11-05",
				"capabilities": map[string]interface{}{
					"tools": map[string]bool{"listChanged": true},
				},
				"serverInfo": map[string]string{
					"name":    "wiki-agent-tools",
					"version": "1.0.0",
				},
			},
		}

	case "tools/list":
		tools := make([]ToolDefinition, 0)
		for name := range s.tools {
			tools = append(tools, ToolDefinition{
				Name:        name,
				Description: fmt.Sprintf("Tool: %s", name),
			})
		}
		return JSONRPCResponse{JSONRPC: "2.0", ID: req.ID, Result: map[string]interface{}{"tools": tools}}

	case "tools/call":
		var params struct {
			Name      string          `json:"name"`
			Arguments json.RawMessage `json:"arguments"`
		}
		json.Unmarshal(req.Params, &params)

		handler, ok := s.tools[params.Name]
		if !ok {
			return JSONRPCResponse{JSONRPC: "2.0", ID: req.ID, Error: &RPCError{Code: -1, Message: "tool not found"}}
		}

		result, err := handler(params.Arguments)
		if err != nil {
			return JSONRPCResponse{JSONRPC: "2.0", ID: req.ID, Error: &RPCError{Code: -1, Message: err.Error()}}
		}

		return JSONRPCResponse{
			JSONRPC: "2.0",
			ID:      req.ID,
			Result: map[string]interface{}{
				"content": []map[string]string{{"type": "text", "text": result}},
			},
		}

	default:
		return JSONRPCResponse{JSONRPC: "2.0", ID: req.ID, Error: &RPCError{Code: -32601, Message: "method not found"}}
	}
}

// 使用示例
func main() {
	server := NewMCPServer()

	// 注册工具
	server.RegisterTool("search_wiki", "搜索 Wiki 知识库", func(args json.RawMessage) (string, error) {
		var params struct {
			Query string `json:"query"`
		}
		json.Unmarshal(args, &params)
		// 实际实现:调用向量数据库搜索
		return fmt.Sprintf("搜索结果: %s", params.Query), nil
	})

	server.RegisterTool("get_file", "读取项目文件", func(args json.RawMessage) (string, error) {
		var params struct {
			Path string `json:"path"`
		}
		json.Unmarshal(args, &params)
		data, err := os.ReadFile(params.Path)
		if err != nil {
			return "", err
		}
		return string(data), nil
	})

	server.Run()
}

Agent 可观测性与调试 ​

Agent 执行追踪 ​

mermaid
graph TD
    subgraph "Agent 可观测性"
        TRACE["执行追踪<br/>每步的输入/输出/耗时"] --> LOG["结构化日志<br/>JSON 格式,可搜索"]
        LOG --> METRIC["指标监控<br/>成功率/延迟/Token 消耗"]
        METRIC --> ALERT["告警<br/>异常检测+通知"]
    end

    style TRACE fill:#3498db,color:#fff
    style METRIC fill:#2ecc71,color:#fff
python
"""
Agent 执行追踪系统
"""

import time
import uuid
from dataclasses import dataclass, field
from typing import List, Optional
from contextlib import contextmanager


@dataclass
class TraceSpan:
    """追踪跨度 — 记录一次操作"""
    span_id: str = ""
    parent_id: str = ""
    name: str = ""           # 操作名称
    start_time: float = 0
    end_time: float = 0
    duration_ms: float = 0
    status: str = "ok"       # ok / error
    attributes: dict = field(default_factory=dict)
    events: List[dict] = field(default_factory=list)

    def __post_init__(self):
        if not self.span_id:
            self.span_id = str(uuid.uuid4())[:8]


@dataclass
class AgentTrace:
    """完整的 Agent 执行追踪"""
    trace_id: str = ""
    spans: List[TraceSpan] = field(default_factory=list)
    total_tokens: int = 0
    total_cost: float = 0.0
    total_duration_ms: float = 0

    def __post_init__(self):
        if not self.trace_id:
            self.trace_id = str(uuid.uuid4())[:12]


class AgentTracer:
    """Agent 追踪器"""

    def __init__(self):
        self.current_trace: Optional[AgentTrace] = None
        self._span_stack: List[TraceSpan] = []

    @contextmanager
    def trace(self, name: str):
        """开始一个新的追踪"""
        self.current_trace = AgentTrace()
        start = time.perf_counter()
        try:
            yield self.current_trace
        finally:
            self.current_trace.total_duration_ms = (time.perf_counter() - start) * 1000

    @contextmanager
    def span(self, name: str, **attributes):
        """记录一个操作跨度"""
        span = TraceSpan(
            name=name,
            start_time=time.perf_counter(),
            parent_id=self._span_stack[-1].span_id if self._span_stack else "",
            attributes=attributes,
        )
        self._span_stack.append(span)
        try:
            yield span
        except Exception as e:
            span.status = "error"
            span.events.append({"error": str(e)})
            raise
        finally:
            span.end_time = time.perf_counter()
            span.duration_ms = (span.end_time - span.start_time) * 1000
            self._span_stack.pop()
            if self.current_trace:
                self.current_trace.spans.append(span)

    def log_llm_call(self, span: TraceSpan, prompt_tokens: int,
                     completion_tokens: int, model: str):
        """记录 LLM 调用信息"""
        span.attributes.update({
            "llm.model": model,
            "llm.prompt_tokens": prompt_tokens,
            "llm.completion_tokens": completion_tokens,
            "llm.total_tokens": prompt_tokens + completion_tokens,
        })
        if self.current_trace:
            self.current_trace.total_tokens += prompt_tokens + completion_tokens

    def export(self) -> dict:
        """导出追踪数据(可发送到 Jaeger/Zipkin)"""
        if not self.current_trace:
            return {}
        return {
            "trace_id": self.current_trace.trace_id,
            "total_duration_ms": self.current_trace.total_duration_ms,
            "total_tokens": self.current_trace.total_tokens,
            "spans": [
                {
                    "span_id": s.span_id,
                    "parent_id": s.parent_id,
                    "name": s.name,
                    "duration_ms": s.duration_ms,
                    "status": s.status,
                    "attributes": s.attributes,
                }
                for s in self.current_trace.spans
            ],
        }


# 使用示例
# tracer = AgentTracer()
# with tracer.trace("user_query"):
#     with tracer.span("classify_task") as span:
#         # 分类任务...
#         pass
#     with tracer.span("llm_call", model="gpt-5.6") as span:
#         # 调用 LLM...
#         tracer.log_llm_call(span, 500, 200, "gpt-5.6")
#     with tracer.span("tool_call", tool="search") as span:
#         # 调用工具...
#         pass
# print(json.dumps(tracer.export(), indent=2))

Agent 技术选型对比 ​

框架语言核心特点适用场景学习曲线
LangChainPython组件丰富、生态最大快速原型中
LangGraphPython图结构、状态管理复杂工作流高
CrewAIPythonMulti-Agent、角色扮演团队协作任务低
AutoGenPython多 Agent 对话研究/实验中
Semantic KernelC#/Python微软生态、企业级.NET 项目中
自研 (Go/Python)Go/Python完全可控、无依赖生产环境高

建议:原型阶段用 LangGraph 快速验证,生产环境用自研框架(如本文的 Go/Python 实现)确保可控性和性能。


Agent 长期记忆 — 持久化的知识与经验 ​

短期记忆(Session 上下文压缩)解决的是单次对话的上下文窗口问题,但当 Agent 需要跨会话记住用户偏好、积累领域知识、从历史任务中学习时,就需要长期记忆系统。

记忆层级架构 ​

mermaid
graph TD
    subgraph "Agent 记忆层级"
        WM["工作记忆<br/>当前对话上下文<br/>存活: 单次会话"] --> STM["短期记忆<br/>对话摘要 + 关键事实<br/>存活: 数小时到数天"]
        STM --> LTM["长期记忆<br/>用户偏好 + 领域知识<br/>存活: 永久"]
        LTM --> EM["情景记忆<br/>历史任务经验<br/>存活: 永久"]
    end

    WM -->|"超出窗口"| COMPRESS["压缩归档"]
    COMPRESS --> STM
    STM -->|"模式识别"| EXTRACT["知识沉淀"]
    EXTRACT --> LTM
    LTM -->|"相似任务"| RECALL["经验召回"]
    RECALL --> WM

    style WM fill:#3498db,color:#fff
    style LTM fill:#e74c3c,color:#fff
    style EM fill:#9b59b6,color:#fff

实现方案:向量记忆 + 结构化事实 ​

python
"""
Agent 长期记忆系统

双层存储:
1. 向量记忆 — 语义检索历史对话片段
2. 结构化事实 — 精确存储用户偏好、实体关系
"""

from dataclasses import dataclass, field
from typing import List, Dict, Optional, Any
from datetime import datetime
import json


@dataclass
class Memory:
    """记忆单元"""
    id: str
    content: str                    # 记忆内容
    memory_type: str                # preference / fact / experience / conversation
    importance: float = 0.5         # 重要性评分 0-1
    created_at: float = 0.0
    last_accessed: float = 0.0
    access_count: int = 0
    metadata: Dict[str, Any] = field(default_factory=dict)

    def __post_init__(self):
        if not self.created_at:
            self.created_at = datetime.now().timestamp()


class LongTermMemory:
    """长期记忆系统"""

    def __init__(self, vector_store, llm_client, kv_store=None):
        """
        Args:
            vector_store: 向量数据库(Milvus/Qdrant/Chroma),用于语义检索
            llm_client: LLM 客户端,用于记忆提取和重要性评分
            kv_store: 键值存储(Redis/SQLite),用于结构化事实
        """
        self.vector_store = vector_store
        self.llm = llm_client
        self.kv_store = kv_store or {}

    # ===== 记忆写入 =====
    async def add_memory(
        self,
        content: str,
        memory_type: str = "conversation",
        metadata: Dict = None,
    ) -> Memory:
        """添加一条记忆"""
        # 1. 评估重要性
        importance = await self._rate_importance(content, memory_type)

        # 2. 创建记忆单元
        memory = Memory(
            id=f"mem_{datetime.now().strftime('%Y%m%d%H%M%S')}_{hash(content) % 10000}",
            content=content,
            memory_type=memory_type,
            importance=importance,
            metadata=metadata or {},
        )

        # 3. 写入向量存储(语义检索)
        await self.vector_store.insert(
            id=memory.id,
            text=content,
            metadata={
                "memory_type": memory_type,
                "importance": importance,
                **memory.metadata,
            },
        )

        # 4. 如果是结构化事实,额外写入 KV 存储
        if memory_type == "fact":
            structured = await self._structure_fact(content)
            if structured:
                key = structured["key"]
                self.kv_store[key] = {
                    "value": structured["value"],
                    "source_memory_id": memory.id,
                    "updated_at": datetime.now().isoformat(),
                }

        return memory

    async def add_from_conversation(self, messages: List[Dict]) -> List[Memory]:
        """从对话中提取并存储记忆"""
        # 用 LLM 从对话中提取值得记住的信息
        conversation_text = "\n".join([
            f"[{m['role']}]: {m['content'][:300]}" for m in messages
        ])

        prompt = f"""从以下对话中提取值得长期记住的信息。返回 JSON 数组,每条包含 content 和 type。

类型说明:
- preference: 用户偏好("用户喜欢简洁的回答")
- fact: 事实信息("项目A的API地址是xxx")
- experience: 经验教训("上次用方案X导致性能问题")

对话:
{conversation_text}

输出格式:
[{{"content": "...", "type": "preference|fact|experience"}}]
"""
        response = await self.llm.chat(prompt, response_format="json")
        items = json.loads(response)

        memories = []
        for item in items:
            memory = await self.add_memory(
                content=item["content"],
                memory_type=item["type"],
            )
            memories.append(memory)

        return memories

    # ===== 记忆检索 =====
    async def recall(
        self,
        query: str,
        top_k: int = 5,
        memory_type: str = None,
        min_importance: float = 0.0,
    ) -> List[Memory]:
        """检索相关记忆"""
        # 1. 向量语义检索
        results = await self.vector_store.search(
            query=query,
            top_k=top_k * 2,  # 多召回,后续过滤
        )

        # 2. 按条件和重要性过滤
        filtered = []
        for r in results:
            meta = r.get("metadata", {})
            if memory_type and meta.get("memory_type") != memory_type:
                continue
            if meta.get("importance", 0) < min_importance:
                continue
            filtered.append(r)

        # 3. 更新访问时间
        now = datetime.now().timestamp()
        for r in filtered[:top_k]:
            r_id = r.get("id")
            # 在向量存储中更新 last_accessed(实际实现取决于向量数据库)

        return filtered[:top_k]

    async def recall_facts(self, entity: str) -> List[Dict]:
        """精确召回关于某个实体的结构化事实"""
        facts = []
        prefix = f"fact:{entity}:"

        for key, value in self.kv_store.items():
            if key.startswith(prefix):
                facts.append({
                    "key": key.replace(prefix, ""),
                    "value": value["value"],
                    "updated_at": value["updated_at"],
                })

        return facts

    # ===== 记忆维护 =====
    async def consolidate(self):
        """记忆巩固:定期将短期记忆沉淀为长期知识"""
        # 1. 获取最近 24h 内访问过但重要性低、访问次数高的记忆
        recent = await self.vector_store.search(
            query="",  # 按时间过滤
            top_k=50,
            filter={"last_accessed": {"$gte": datetime.now().timestamp() - 86400}},
        )

        # 2. 用 LLM 识别可合并/升级的记忆
        if recent:
            content_list = "\n".join([
                f"- [{r.get('metadata', {}).get('memory_type', '?')}] {r['text'][:100]}"
                for r in recent
            ])
            prompt = f"""分析以下最近访问的记忆,找出:
1. 可以合并的重复/相似记忆
2. 应该从短期升级为长期的记忆
3. 可以降低重要性的过时信息

记忆列表:
{content_list}

返回 JSON:
{{"merge": [["id1", "id2", "合并后内容"]], "upgrade": ["id3", "id4"], "downgrade": ["id5"]}}
"""
            response = await self.llm.chat(prompt, response_format="json")
            plan = json.loads(response)

            # 3. 执行合并/升级/降级操作
            await self._execute_consolidation_plan(plan)

    async def forget(self, memory_id: str):
        """删除记忆"""
        await self.vector_store.delete(memory_id)
        # 清除关联的结构化事实(如果有)

    def get_stats(self) -> Dict:
        """记忆系统统计"""
        return {
            "total_vector_memories": len(self.vector_store),
            "total_structured_facts": len(self.kv_store),
        }

    # ===== 内部方法 =====
    async def _rate_importance(self, content: str, memory_type: str) -> float:
        """LLM 评估记忆重要性"""
        prompt = f"""评估以下信息的重要性(0-1之间的浮点数):

类型: {memory_type}
内容: {content[:200]}

评分标准:
- 0.8-1.0: 关键信息(用户核心偏好、系统配置、安全规则)
- 0.5-0.7: 有价值信息(项目细节、经验教训)
- 0.2-0.4: 一般信息(日常对话、临时讨论)
- 0.0-0.1: 琐碎信息

只返回数字。"""
        response = await self.llm.chat(prompt)
        try:
            return float(response.strip())
        except ValueError:
            return 0.5

    async def _structure_fact(self, content: str) -> Optional[Dict]:
        """将自然语言事实转为结构化键值"""
        prompt = f"""将以下事实转为结构化键值对,key 格式为 "fact:{实体}:{属性}":

事实: {content}

返回 JSON:
{{"key": "fact:xxx:yyy", "value": "具体值"}}

如果无法结构化,返回 null。"""
        response = await self.llm.chat(prompt, response_format="json")
        return json.loads(response)

    async def _execute_consolidation_plan(self, plan: Dict):
        """执行记忆巩固计划"""
        for items in plan.get("merge", []):
            # 合并多条记忆为一条
            merged_content = items[2] if len(items) > 2 else ""
            if merged_content:
                await self.add_memory(merged_content, "experience", importance=0.7)
            for mid in items[:2]:
                await self.forget(mid)

        for mid in plan.get("upgrade", []):
            # 升级为长期记忆(实际实现取决于向量数据库的元数据更新)
            pass

        for mid in plan.get("downgrade", []):
            # 降级重要性
            pass

记忆检索集成到 Agent 循环 ​

python
class MemoryAwareAgent:
    """集成长期记忆的 Agent"""

    def __init__(self, llm_client, tools, memory: LongTermMemory):
        self.llm = llm_client
        self.tools = tools
        self.memory = memory
        self.conversation_history = []  # 短期工作记忆

    async def think(self, user_message: str) -> str:
        """带有记忆增强的思考过程"""

        # 1. 从长期记忆中检索相关信息
        relevant_memories = await self.memory.recall(
            query=user_message,
            top_k=3,
        )

        # 2. 检索用户偏好
        user_preferences = await self.memory.recall(
            query=user_message,
            memory_type="preference",
            top_k=2,
        )

        # 3. 检索相关结构化事实
        entities = await self._extract_entities(user_message)
        structured_facts = {}
        for entity in entities:
            facts = await self.memory.recall_facts(entity)
            if facts:
                structured_facts[entity] = facts

        # 4. 构建增强的上下文
        memory_context = self._build_memory_context(
            relevant_memories, user_preferences, structured_facts
        )

        # 5. 标准 Agent 推理
        system_prompt = f"""你是一个有记忆的 AI Agent。参考以下记忆来回答用户问题。

## 用户偏好
{memory_context['preferences']}

## 相关知识
{memory_context['knowledge']}

## 相关经验
{memory_context['experiences']}"""

        response = await self.llm.chat(
            system=system_prompt,
            messages=self.conversation_history + [{"role": "user", "content": user_message}],
        )

        # 6. 更新工作记忆
        self.conversation_history.append({"role": "user", "content": user_message})
        self.conversation_history.append({"role": "assistant", "content": response})

        # 7. 异步:从对话中提取新记忆(不阻塞主流程)
        asyncio.create_task(
            self.memory.add_from_conversation(self.conversation_history[-4:])
        )

        return response

    def _build_memory_context(
        self, memories, preferences, facts
    ) -> Dict[str, str]:
        """构建记忆上下文"""
        return {
            "preferences": "\n".join([
                f"- {m.content}" for m in preferences
            ]) or "无",
            "knowledge": "\n".join([
                f"- {m.content}" for m in memories if m.memory_type != "experience"
            ]) or "无",
            "experiences": "\n".join([
                f"- {m.content}" for m in memories if m.memory_type == "experience"
            ]) or "无",
        }

记忆系统的工程考量 ​

考量点方案说明
检索延迟异步预取 + 缓存长期记忆检索不阻塞主推理流程
存储增长TTL + 重要性衰减低重要性记忆自动过期,控制存储成本
隐私合规按用户隔离 + 遗忘权支持用户级记忆删除,符合 GDPR
冷启动默认偏好模板新用户无记忆时使用通用默认值
多会话一致性分布式 KV 存储 (Redis)结构化事实跨服务实例共享

核心原则:长期记忆不是"把所有对话都存下来",而是"提取值得记住的,遗忘不值得的"。好的记忆系统应该像人类一样,重要的记得牢,琐碎的自动淡忘。


原生多模态 Agent — 视觉 + 听觉 + 行动的统一体 ​

范式跃迁:从"文本为主"到"原生多模态" ​

GPT-5.6 和 Claude Fable 这一代模型的根本不同在于:它们不再把图片/音频当作"文本的附件",而是原生理解多模态输入。Agent 的感知和行动能力因此大幅扩展:

mermaid
graph TD
    subgraph "上一代 Agent (GPT-4o 时代)"
        T1["文本输入"] --> AG1["Agent 推理"]
        IMG1["图片输入<br/>(需要预处理)"] -.->|"转文本描述"| AG1
        AUD1["音频输入<br/>(需要 STT)"] -.->|"转文字"| AG1
        AG1 --> OUT1["文本输出<br/>+ 函数调用"]
    end

    subgraph "新一代 Agent (GPT-5.6 / Claude Fable)"
        TXT["文本"] --> AG2["Agent 推理引擎<br/>统一多模态理解"]
        IMG["图像/视频帧"] --> AG2
        AUD["音频流"] --> AG2
        CODE["代码/表格"] --> AG2
        AG2 --> TOUT["文本 + 结构化输出"]
        AG2 --> VOUT["图像生成/编辑"]
        AG2 --> ACT["Computer Use<br/>鼠标/键盘操作"]
        AG2 --> AOUT["语音合成输出"]
    end

    style AG2 fill:#e74c3c,color:#fff
    style ACT fill:#9b59b6,color:#fff

多模态 Agent 工程实现 ​

python
"""
原生多模态 Agent — 统一处理文本、图像、音频、代码
"""

from dataclasses import dataclass, field
from typing import List, Dict, Optional, Union
from enum import Enum
import base64
import json


class ContentType(Enum):
    TEXT = "text"
    IMAGE = "image"
    AUDIO = "audio"
    VIDEO_FRAME = "video_frame"
    CODE = "code"
    TABLE = "table"


@dataclass
class MultiModalContent:
    """多模态内容块"""
    type: ContentType
    data: Union[str, bytes]        # 文本内容 或 base64 编码的媒体
    mime_type: str = ""            # image/png, audio/wav 等
    metadata: Dict = field(default_factory=dict)  # 分辨率、时长等


class MultiModalAgent:
    """原生多模态 Agent"""

    def __init__(self, llm_client, tools: List = None):
        self.llm = llm_client
        self.tools = tools or []

    def build_message(
        self, user_input: Union[str, List[MultiModalContent]]
    ) -> dict:
        """构建多模态消息"""
        if isinstance(user_input, str):
            return {"role": "user", "content": user_input}

        # 多模态消息:多个 content block
        content_blocks = []

        for block in user_input:
            if block.type == ContentType.TEXT:
                content_blocks.append({
                    "type": "text",
                    "text": block.data,
                })
            elif block.type == ContentType.IMAGE:
                # 支持本地文件路径或 base64
                if isinstance(block.data, str) and not block.data.startswith("data:"):
                    with open(block.data, "rb") as f:
                        img_data = base64.b64encode(f.read()).decode()
                else:
                    img_data = block.data if isinstance(block.data, str) else \
                              base64.b64encode(block.data).decode()

                content_blocks.append({
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": block.mime_type or "image/png",
                        "data": img_data,
                    },
                })
            elif block.type == ContentType.AUDIO:
                if isinstance(block.data, str):
                    with open(block.data, "rb") as f:
                        audio_data = base64.b64encode(f.read()).decode()
                else:
                    audio_data = base64.b64encode(block.data).decode()

                content_blocks.append({
                    "type": "audio",
                    "source": {
                        "type": "base64",
                        "media_type": block.mime_type or "audio/wav",
                        "data": audio_data,
                    },
                })
            elif block.type == ContentType.VIDEO_FRAME:
                # 视频帧作为图片序列发送
                content_blocks.append({
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/jpeg",
                        "data": block.data,
                    },
                    "metadata": {"frame_index": block.metadata.get("frame_index", 0)},
                })

        return {"role": "user", "content": content_blocks}

    async def perceive_and_act(
        self, input_contents: List[MultiModalContent], task: str
    ) -> Dict:
        """多模态感知 + 行动"""
        # 1. 构建消息
        message = self.build_message(input_contents)

        # 2. 添加任务指令
        if isinstance(message["content"], list):
            message["content"].insert(0, {
                "type": "text",
                "text": f"[任务] {task}\n\n请分析以上多模态内容并执行任务。",
            })

        # 3. 调用 LLM(Native Tool Use)
        response = await self.llm.chat(
            messages=[message],
            tools=self.tools,
            tool_choice="auto",
        )

        return response

    # ===== 常见多模态场景 =====

    async def analyze_ui_screenshot(
        self, screenshot_path: str, task: str = "描述这个界面并找出所有可交互元素"
    ) -> Dict:
        """分析 UI 截图"""
        return await self.perceive_and_act(
            [MultiModalContent(
                type=ContentType.IMAGE,
                data=screenshot_path,
                mime_type="image/png",
            )],
            task,
        )

    async def debug_from_screenshot(
        self, screenshot_path: str, error_description: str
    ) -> Dict:
        """从截图诊断错误"""
        return await self.perceive_and_act(
            [
                MultiModalContent(
                    type=ContentType.IMAGE, data=screenshot_path, mime_type="image/png",
                ),
                MultiModalContent(
                    type=ContentType.TEXT,
                    data=f"用户描述的错误: {error_description}",
                ),
            ],
            "分析截图中的错误信息,给出修复方案",
        )

    async def review_design(
        self, design_image: str, requirements: str
    ) -> Dict:
        """设计稿审查"""
        return await self.perceive_and_act(
            [
                MultiModalContent(
                    type=ContentType.IMAGE, data=design_image, mime_type="image/png",
                ),
                MultiModalContent(
                    type=ContentType.TEXT, data=f"需求文档: {requirements}",
                ),
            ],
            "对比设计稿和需求文档,找出不一致之处并给出修改建议",
        )

    async def transcribe_and_analyze(
        self, audio_path: str
    ) -> Dict:
        """音频转写+分析(原生,不需要 STT 预处理)"""
        return await self.perceive_and_act(
            [MultiModalContent(
                type=ContentType.AUDIO,
                data=audio_path,
                mime_type="audio/mp3",
            )],
            "转写这段音频,然后提取关键信息和待办事项",
        )

    async def analyze_video(
        self, video_path: str, analysis_task: str, fps: float = 1.0
    ) -> Dict:
        """视频分析 — 按帧率采样"""
        import cv2

        frames = []
        cap = cv2.VideoCapture(video_path)
        frame_interval = int(cap.get(cv2.CAP_PROP_FPS) / fps) if fps > 0 else 30
        frame_idx = 0

        while True:
            ret, frame = cap.read()
            if not ret:
                break
            if frame_idx % frame_interval == 0:
                _, buffer = cv2.imencode('.jpg', frame)
                frames.append(MultiModalContent(
                    type=ContentType.VIDEO_FRAME,
                    data=base64.b64encode(buffer).decode(),
                    mime_type="image/jpeg",
                    metadata={"frame_index": frame_idx, "timestamp": frame_idx / cap.get(cv2.CAP_PROP_FPS)},
                ))
            frame_idx += 1

        cap.release()

        return await self.perceive_and_act(
            frames + [MultiModalContent(type=ContentType.TEXT, data=analysis_task)],
            f"分析视频内容({len(frames)} 帧),完成: {analysis_task}",
        )

多模态 Agent 场景矩阵 ​

场景输入Agent 能力典型应用
UI 自动化测试截图 + 操作描述识别按钮/文本/布局 → 生成操作指令视觉回归测试、无障碍检查
客服质检聊天截图/录音识别情绪、检测违规用语客服质量分析
代码审查增强代码 + UI 截图对比代码逻辑与实际渲染PR Review 可视化
会议纪要音频 + 共享屏幕转写 + 提取决策 + 生成待办智能会议助手
文档理解PDF/扫描件(含图表)同时理解文字和图表合同审查、发票识别
视频内容审核视频流逐帧分析 + 时间轴标注内容安全审核

Computer Use / Code Agent — 模型直接操作世界的时代 ​

概念 ​

2025-2026 年,Agent 的边界从"调用 API"扩展到"直接操作计算机"。Claude Fable 的 Computer Use 和 OpenAI 的 Operator 将屏幕截图→鼠标键盘操作作为 Agent 的原生能力:

mermaid
graph TD
    subgraph "传统 Agent"
        U1["用户指令"] --> LC1["LLM 推理"] --> TC1["Tool Call<br/>API/命令行"]
    end

    subgraph "Computer Use Agent"
        U2["用户指令"] --> LC2["LLM 推理"] --> ACT2["操作计算机<br/>🖱️ 移动鼠标<br/>⌨️ 键盘输入<br/>🖥️ 截屏验证"]
        ACT2 -->|"获取新截图"| OBS["观察环境变化"]
        OBS --> LC2
    end

    style ACT2 fill:#e74c3c,color:#fff
    style OBS fill:#f39c12,color:#fff

Computer Use 实现 ​

python
"""
Computer Use Agent — 模型控制鼠标键盘
"""

from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple
import pyautogui
import base64
import io
import time


@dataclass
class ComputerAction:
    """计算机操作"""
    action_type: str  # click, type, scroll, screenshot, key_press, drag, wait
    params: Dict = None


class ComputerUseAgent:
    """Computer Use Agent — 自动化操作桌面/浏览器"""

    def __init__(self, llm_client):
        self.llm = llm_client
        pyautogui.FAILSAFE = True  # 鼠标移到角落时停止
        pyautogui.PAUSE = 0.5       # 每个操作后暂停

    def get_screenshot(self) -> str:
        """截取当前屏幕,返回 base64"""
        screenshot = pyautogui.screenshot()
        buffer = io.BytesIO()
        screenshot.save(buffer, format="PNG")
        return base64.b64encode(buffer.getvalue()).decode()

    def get_screen_size(self) -> Tuple[int, int]:
        """获取屏幕分辨率"""
        return pyautogui.size()

    def execute_action(self, action: ComputerAction) -> Dict:
        """执行单个操作"""
        result = {"success": True, "action": action.action_type}

        try:
            if action.action_type == "click":
                x, y = action.params["x"], action.params["y"]
                # 坐标归一化:模型可能返回 0-1 的相对坐标
                if x < 1 and y < 1:
                    w, h = self.get_screen_size()
                    x, y = int(x * w), int(y * h)
                pyautogui.click(x, y)
                result["position"] = (x, y)

            elif action.action_type == "type":
                text = action.params["text"]
                pyautogui.write(text)
                result["text"] = text

            elif action.action_type == "key_press":
                keys = action.params["keys"]  # 如 "enter", "ctrl+c"
                pyautogui.hotkey(*keys.split("+"))
                result["keys"] = keys

            elif action.action_type == "scroll":
                amount = action.params.get("amount", 3)
                pyautogui.scroll(amount)
                result["amount"] = amount

            elif action.action_type == "drag":
                x1, y1 = action.params["x1"], action.params["y1"]
                x2, y2 = action.params["x2"], action.params["y2"]
                pyautogui.moveTo(x1, y1)
                pyautogui.drag(x2 - x1, y2 - y1, duration=0.5)

            elif action.action_type == "screenshot":
                result["image"] = self.get_screenshot()

            elif action.action_type == "wait":
                duration = action.params.get("duration", 1)
                time.sleep(duration)

            elif action.action_type == "move":
                x, y = action.params["x"], action.params["y"]
                pyautogui.moveTo(x, y)
                result["position"] = (x, y)

            else:
                result["success"] = False
                result["error"] = f"Unknown action: {action.action_type}"

        except Exception as e:
            result["success"] = False
            result["error"] = str(e)

        return result

    async def run_task(self, task: str, max_steps: int = 20) -> Dict:
        """执行一个完整的 Computer Use 任务"""
        history = []
        screenshot_before = self.get_screenshot()

        for step in range(max_steps):
            # 1. 发送截图 + 任务 + 历史给模型
            message = {
                "role": "user",
                "content": [
                    {"type": "text", "text": f"[任务] {task}\n\n你正在操作一台 {self.get_screen_size()} 分辨率的计算机。\n请决定下一步操作。只输出 JSON:\n{{\"action\": \"click|type|scroll|key_press|done\", \"params\": {{...}}, \"reasoning\": \"为什么做这个操作\"}}"},
                    {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": screenshot_before}},
                ],
            }

            if history:
                message["content"].insert(0, {
                    "type": "text",
                    "text": f"历史操作: {json.dumps(history[-3:], ensure_ascii=False)}",
                })

            # 2. 获取模型决策
            response = await self.llm.chat(
                messages=[message],
                response_format="json",
            )
            decision = json.loads(response)

            # 3. 如果任务完成
            if decision.get("action") == "done":
                return {
                    "success": True,
                    "steps": len(history),
                    "history": history,
                    "conclusion": decision.get("params", {}).get("result", "Task completed"),
                }

            # 4. 执行操作
            action = ComputerAction(
                action_type=decision["action"],
                params=decision.get("params", {}),
            )
            result = self.execute_action(action)
            result["reasoning"] = decision.get("reasoning", "")
            history.append(result)

            # 5. 截取操作后的屏幕
            time.sleep(0.5)  # 等待界面更新
            screenshot_before = self.get_screenshot()

        return {
            "success": False,
            "steps": len(history),
            "history": history,
            "error": f"Reached max steps ({max_steps}) without completing task",
        }


# ===== Code Agent — 模型直接写代码并执行 =====
class CodeAgent:
    """Code Agent — 在隔离环境中编写和执行代码"""

    def __init__(self, llm_client, workspace: str = "/tmp/code_agent"):
        self.llm = llm_client
        self.workspace = workspace
        import os
        os.makedirs(workspace, exist_ok=True)

    async def solve_with_code(self, task: str, max_iterations: int = 5) -> Dict:
        """用代码解决问题:写→执行→观察→修改→再执行"""
        iteration = 0
        history = []

        while iteration < max_iterations:
            iteration += 1

            # 1. 模型生成/修改代码
            if history:
                last_error = history[-1].get("error", "")
                last_output = history[-1].get("output", "")
                prompt = f"""[任务] {task}

上次执行的输出:
{last_output}

上次的错误:
{last_error}

请修改代码以修复错误并完成任务。只输出代码块。"""
            else:
                prompt = f"[任务] {task}\n\n请写 Python 代码完成任务。只输出代码块。"

            response = await self.llm.chat(prompt)
            code = self._extract_code(response)

            # 2. 保存并执行
            filepath = f"{self.workspace}/solution_{iteration}.py"
            with open(filepath, "w") as f:
                f.write(code)

            exec_result = await self._execute_code(filepath)

            history.append({
                "iteration": iteration,
                "file": filepath,
                "code": code,
                "output": exec_result.get("stdout", ""),
                "error": exec_result.get("stderr", ""),
                "success": exec_result.get("returncode") == 0,
            })

            # 3. 检查是否成功
            if exec_result.get("returncode") == 0 and not exec_result.get("stderr"):
                return {"success": True, "history": history, "final_code": code}

        return {"success": False, "history": history, "error": "Max iterations reached"}

    def _extract_code(self, text: str) -> str:
        import re
        m = re.search(r'```(?:python)?\n(.*?)```', text, re.DOTALL)
        return m.group(1) if m else text

    async def _execute_code(self, filepath: str, timeout: int = 30) -> Dict:
        import subprocess
        try:
            result = subprocess.run(
                ["python", filepath],
                capture_output=True,
                text=True,
                timeout=timeout,
                cwd=self.workspace,
            )
            return {
                "returncode": result.returncode,
                "stdout": result.stdout[-2000:],
                "stderr": result.stderr[-2000:],
            }
        except subprocess.TimeoutExpired:
            return {"returncode": -1, "stdout": "", "stderr": "Execution timed out"}
        except Exception as e:
            return {"returncode": -1, "stdout": "", "stderr": str(e)}

Computer Use Agent 的安全边界 ​

安全层措施说明
沙箱执行Docker / VM 隔离在隔离环境中操作,不触碰宿主机
操作白名单允许的操作类型列表禁止 rm -rf、format 等危险操作
人类确认关键操作前请求确认涉及文件删除、网络请求时弹窗确认
操作审计全量录屏 + 日志每次 Computer Use 会话完整录像
速率限制限制操作频率防止过快操作导致不可逆后果
回滚能力VM 快照操作出错后一键恢复到操作前状态

典型应用场景 ​

场景Agent 类型示例
Web 自动化测试Computer Use"打开登录页,输入账号密码,点击登录,验证跳转到首页"
数据录入Computer Use"从这个 Excel 表格读取数据,逐行录入到网页表单"
RPA 替代Computer Use"打开邮件客户端,查找未读邮件,提取附件保存"
数据分析Code Agent"读取 CSV,做统计,生成图表,输出结论"
代码迁移Code Agent"将这个 Python 2 项目升级到 Python 3,修复所有兼容性问题"
Bug 修复Code Agent"运行测试,定位失败原因,修改代码,确认测试通过"

核心原则:Computer Use 和 Code Agent 不是要替代所有 API 集成,而是填补"没有 API 可调"的空白。有 API 就用 API(高效、稳定),没有 API 时才用屏幕操作(通用、但慢且脆弱)。

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记