犀牛鸟 2026 研究笔记 estelledc.github.io

第二章:tRPC-Agent-Go 全架构精读

本章目标:理解 tRPC-Agent-Go 的三层架构(Runner → LLM Flow → Agent Interface),搞清楚每一层的职责边界和数据流向。


2.1 三层套娃:从请求到执行

tRPC-Agent-Go 的执行架构是一个三层嵌套结构。理解它的关键是搞清楚每一层负责什么、不负责什么

用户请求 → Runner(接待 + 管理)
              ↓
         LLM Flow(ReAct 循环)
              ↓
         Agent Interface(能力定义)

Runner 层(3971 行)—— “餐厅前台”

Runner 是用户和 Agent 之间的中间人。它负责:

  1. Session 管理:同一个用户的多轮对话归属同一个 session
  2. 请求生命周期:生成 RequestID、注册/取消/查询运行状态
  3. 事件循环:持续监听 Agent 产出的事件流,做后处理
  4. 消息持久化:把每轮对话存入 session storage
  5. Agent 选择:多 Agent 场景下选择哪个 Agent 执行

Runner 不负责:具体的 LLM 调用逻辑、工具执行逻辑、思考策略。

日常类比:餐厅前台。她负责接待客人(接收请求)、分配座位(选择 Agent)、记录点单(session 持久化)、确认是否取消(Cancel)、通知菜好了(事件分发)。但她不做菜。

LLM Flow 层(2468 行)—— “厨师的工作循环”

LLM Flow 是真正的 ReAct 循环执行者。它负责:

  1. 构建 LLM 请求:把 session 历史、系统指令、可用工具注入请求
  2. 调用 LLM:发送请求,处理流式响应
  3. 工具执行:如果 LLM 返回 tool_call,执行对应工具
  4. 循环控制:决定是否继续下一轮(还有 tool_call?Agent 说结束了?超过最大步数?)
  5. 事件发射:每一步产出对应的事件

LLM Flow 不负责:session 管理、请求生命周期、多 Agent 路由。

日常类比:厨师的工作流程。看菜单(构建请求)→ 备料炒菜(调 LLM)→ 如果需要配菜就去拿(执行工具)→ 把配菜加进去继续炒(结果追加到会话)→ 直到出餐(final response)。

Agent Interface 层(105 行)—— “厨师的资质证明”

Agent Interface 是一个声明性接口——它不执行任何逻辑,只描述”我是谁、我能做什么”:

type Agent interface {
    Run(ctx context.Context, invocation *Invocation) (<-chan *event.Event, error)
    Tools() []tool.Tool
    Info() Info
    SubAgents() []Agent
    FindSubAgent(name string) Agent
}

Agent Interface 不负责:具体的执行策略。不同的 Agent 实现(LLMAgent、GraphAgent、ChainAgent、ParallelAgent)各自决定 Run 方法内部的逻辑。

日常类比:厨师的资质证书。上面写了”姓名(Info)”“会做什么菜(Tools)”“有没有助手(SubAgents)”。但怎么做菜(Run 的具体实现)取决于每个厨师自己的风格。


2.2 核心数据流:一次请求的完整旅程

让我们跟踪一次用户请求,看它如何穿越三层架构:

1. 用户发送 "帮我搜索今天的新闻并写一个摘要"
       ↓
2. Runner.Run() 接收请求
   - 解析选项,生成 RequestID
   - 获取/创建 Session(载入历史对话)
   - 选择目标 Agent
   - 构建 Invocation 对象
   - 注册运行句柄(支持 Cancel)
       ↓
3. agent.RunWithPlugins() 启动 Agent
   - 执行 pre-run 插件链(日志、鉴权、限流...)
   - 调用 agent.Run(ctx, invocation)
       ↓
4. LLMAgent.Run() 创建 LLM Flow 并启动
       ↓
5. LLM Flow 进入 ReAct 循环:
   【第 1 轮】
   - 构建 LLM 请求:系统指令 + 历史 + "帮我搜索..." + 工具列表
   - 调 LLM → 返回 tool_call: search("今天新闻")
   - 执行 search 工具 → 得到新闻列表
   - 结果追加到 session
   【第 2 轮】
   - 构建 LLM 请求:上次的内容 + 搜索结果
   - 调 LLM → 返回 final text: "今日新闻摘要:..."
   - 发射 FinalResponse 事件
   - 退出循环
       ↓
6. Runner 事件循环收到 FinalResponse
   - 持久化本轮消息到 Session
   - 执行 post-run 插件链
   - 向调用方返回事件流
       ↓
7. 用户收到 "今日新闻摘要:..."

2.3 Invocation:执行上下文对象

Invocation 是贯穿整次执行的上下文对象(2339 行),它携带了 Agent 执行所需的一切信息:

type Invocation struct {
    // 身份:谁在执行
    Agent      Agent
    AgentInfo  *AgentInfo

    // 输入:用户说了什么
    Messages   []*Message
    UserID     string
    SessionID  string

    // 状态:当前进展
    State      *State
    CurrentStep int
    EndInvocation bool   // Agent 主动标记"我做完了"

    // 能力:能用什么
    Tools      []Tool
    SubAgents  []Agent

    // 控制:约束条件
    MaxSteps   int
    Plan       *Plan     // 预规划的步骤

    // 可观测性:追踪信息
    TraceID    string
    SpanID     string
    RequestID  string
}

日常类比:Invocation 就像一张工单。上面写了:谁发起的(UserID)、什么任务(Messages)、交给谁做(Agent)、能用什么工具(Tools)、最多做多少步(MaxSteps)、当前进度如何(CurrentStep)。工单在各层之间传递,每一层都往上面写进展。

2.3.5 State 对象深度解析

StateInvocation 中最容易被忽略、但最重要的字段之一。它是节点之间传递自定义数据的载体。

// State 的核心结构
type State struct {
    // Values 是一个通用 map —— 节点可以往里面写任何 key-value
    Values   map[string]interface{}

    // Metadata 是系统级元数据 —— 用户代码不应直接修改
    Metadata *StateMetadata
}

type StateMetadata struct {
    CreatedAt    time.Time
    UpdatedAt    time.Time
    Version      int64      // 乐观锁版本号
    CheckpointID string    // 关联的 checkpoint
}

State 的五种使用场景

// 场景1:节点间传递中间结果
func researchNode(ctx context.Context, state *State) error {
    results := doSearch(state.Values["query"].(string))
    state.Values["research_results"] = results  // 写入 state
    return nil
}

func writeNode(ctx context.Context, state *State) error {
    results := state.Values["research_results"].([]string)  // 读取上游产出
    article := generateArticle(results)
    state.Values["article"] = article
    return nil
}

// 场景2:循环计数器(防止死循环)
func qualityCheckNode(ctx context.Context, state *State) error {
    count, _ := state.Values["revision_count"].(int)
    state.Values["revision_count"] = count + 1
    return nil
}

// 场景3:条件路由的判断依据
func routingDecision(state *State) string {
    score := state.Values["quality_score"].(float64)
    if score > 0.8 {
        return "done"
    }
    return "revise"
}

// 场景4:跨 Agent 传递上下文(Team 场景)
func coordinatorNode(ctx context.Context, state *State) error {
    state.Values["task_for_researcher"] = "搜索AI趋势"
    state.Values["task_for_writer"] = "写一篇2000字文章"
    return nil
}

// 场景5:存储工具执行的副作用结果
func toolCallback(state *State, toolName string, result string) {
    key := fmt.Sprintf("tool_%s_%d", toolName, time.Now().UnixNano())
    state.Values[key] = result
}

State vs Session:容易混淆的两个概念

维度 State Session
生命周期 单次请求内 跨多次请求(持久化)
存储位置 内存(Invocation 内) Redis / 数据库
内容 当前执行的中间状态 完整对话历史
谁写 节点函数 / Agent 逻辑 Runner 自动管理
谁读 下游节点 / 路由函数 LLM Flow 构建请求时
类比 工单上的批注 客户档案

关键区别:Session 是”这个用户之前说过什么”(对话记忆),State 是”这次执行进行到哪了”(工作进度)。Session 跨请求持久化,State 通常在单次请求结束后丢弃(除非用了 Checkpoint)。

State 在 StateGraph 中的特殊地位:在 GraphAgent 中,State 就是图的”全局共享内存”——所有节点通过读写 State 来通信。这和 LangGraph 的 Channel 机制是同一个思想:节点之间不直接调用彼此,而是通过共享状态间接通信。这种设计的好处是节点完全解耦——你可以随意添加、删除、重排节点,只要它们读写的 State key 兼容即可。


2.4 Event 流:异步非阻塞的设计哲学

tRPC-Agent-Go 选择用 Event Channel 而非同步返回值来传递执行结果:

// Agent.Run 返回的是一个事件通道,不是一个结果值
func (a *MyAgent) Run(ctx context.Context, inv *Invocation) (<-chan *event.Event, error) {
    eventChan := make(chan *event.Event, bufferSize)
    go func() {
        // 异步执行,产出事件流
        eventChan <- &event.Event{Type: "thinking", Data: "正在分析..."}
        eventChan <- &event.Event{Type: "tool_call", Data: toolCallData}
        eventChan <- &event.Event{Type: "tool_result", Data: resultData}
        eventChan <- &event.Event{Type: "final_response", Data: answer}
        close(eventChan)
    }()
    return eventChan, nil
}

为什么这样设计?三个原因:

  1. 非阻塞:调用方不需要等 Agent 执行完。一个 Agent 可能思考 30 秒甚至更久——如果用同步调用,整个系统就卡住了。
  2. 流式输出:用户可以实时看到 Agent 的思考过程(正在搜索…正在写…),不需要等最终结果出来才有反馈。
  3. 可组合:Runner 的事件循环可以监听多个 Agent 的事件流,做合并、过滤、路由——这是多 Agent 协作的基础。

对比 LangGraph 的设计:LangGraph 的节点函数返回 dict(同步写入 Channel),节点之间通过 Pregel 超步严格串行。这保证了状态一致性,但代价是无法流式输出中间过程——你要等一整个超步完成才能看到结果。


2.5 Plugin 系统:横切关注点的优雅解法

Runner 在调用 Agent 前后会执行 Plugin 链:

// 伪代码:RunWithPlugins 的执行顺序
func RunWithPlugins(ctx, inv, agent) {
    for _, plugin := range plugins {
        plugin.BeforeRun(ctx, inv)  // 前置处理
    }

    eventChan := agent.Run(ctx, inv)  // 实际执行

    for _, plugin := range plugins {
        plugin.AfterRun(ctx, inv)   // 后置处理
    }
}

Plugin 可以做的事情:

Plugin 类型 用途 类比
LoggingPlugin 记录请求/响应日志 门卫登记本
AuthPlugin 验证用户身份和权限 门禁刷卡
RateLimitPlugin 限制每分钟请求数 电梯限载
TracingPlugin 注入链路追踪 span 快递单号
MetricsPlugin 统计延迟/成功率 绩效打分

这和 Java 的 Servlet Filter、Go 的 HTTP Middleware 是同一个设计模式——在不修改核心逻辑的前提下注入横切关注点

2.5.5 Plugin 高级用法

自定义 Plugin 完整实现

// Plugin 接口定义
type Plugin interface {
    BeforeRun(ctx context.Context, inv *Invocation) error
    AfterRun(ctx context.Context, inv *Invocation, err error) error
    OnEvent(ctx context.Context, ev *event.Event) error
    Name() string
}

// 示例:Token 计费 Plugin
type TokenAccountingPlugin struct {
    store   TokenStore
    limiter TokenLimiter
}

func (p *TokenAccountingPlugin) Name() string {
    return "token_accounting"
}

func (p *TokenAccountingPlugin) BeforeRun(ctx context.Context, inv *Invocation) error {
    // 检查用户配额
    remaining, err := p.limiter.GetRemaining(ctx, inv.UserID)
    if err != nil {
        return fmt.Errorf("check quota failed: %w", err)
    }
    if remaining <= 0 {
        return ErrQuotaExhausted  // 返回错误 → 中止执行
    }
    return nil
}

func (p *TokenAccountingPlugin) OnEvent(ctx context.Context, ev *event.Event) error {
    // 监听 LLM 响应事件,实时累计 token
    if ev.Type == event.TypeLLMResponse {
        usage := ev.Data.(*LLMResponseData).TokenUsage
        inv := GetInvocationFromContext(ctx)
        p.store.AddUsage(ctx, inv.UserID, usage.PromptTokens, usage.CompletionTokens)
    }
    return nil
}

func (p *TokenAccountingPlugin) AfterRun(ctx context.Context, inv *Invocation, runErr error) error {
    // 执行结束后记录计费
    totalUsage := p.store.GetSessionUsage(ctx, inv.SessionID)
    cost := calculateCost(totalUsage, inv.AgentInfo.Model)
    p.store.RecordBilling(ctx, inv.UserID, inv.RequestID, cost)
    return nil
}

Plugin 执行顺序:洋葱模型

Plugin 链的执行顺序(类比 HTTP Middleware):

  BeforeRun 按注册顺序:Auth → RateLimit → Logging
  [Agent 执行]
  AfterRun 按注册逆序:Logging → RateLimit → Auth

  请求方向 →  Auth.Before → RateLimit.Before → Logging.Before
                                                     ↓
                                              [Agent.Run()]
                                                     ↓
  响应方向 ←  Auth.After  ← RateLimit.After  ← Logging.After

Plugin 间数据传递

// Plugin 之间通过 Invocation.State 传递数据
func (auth *AuthPlugin) BeforeRun(ctx context.Context, inv *Invocation) error {
    user, err := authenticate(ctx)
    if err != nil {
        return ErrUnauthorized
    }
    // 写入 State,下游 Plugin 和 Agent 都能读到
    inv.State.Values["auth_user"] = user
    inv.State.Values["user_tier"] = user.Tier
    return nil
}

func (rateLimit *RateLimitPlugin) BeforeRun(ctx context.Context, inv *Invocation) error {
    // 读取上游 Auth Plugin 写入的用户信息
    tier := inv.State.Values["user_tier"].(string)
    limit := map[string]int{"free": 10, "pro": 100, "enterprise": 1000}[tier]
    if !p.allow(inv.UserID, limit) {
        return ErrRateLimited
    }
    return nil
}

组合模式:生产级 Plugin Suite

// 把多个 Plugin 打包成一个逻辑单元
func NewProductionSuite(cfg Config) []Plugin {
    return []Plugin{
        NewAuthPlugin(cfg.Auth),
        NewRateLimitPlugin(cfg.RateLimit),
        NewTracingPlugin(cfg.Jaeger),
        NewMetricsPlugin(cfg.Prometheus),
        NewTokenAccountingPlugin(cfg.Billing),
        NewLoggingPlugin(cfg.Logger),  // 放最后:能记录其他 Plugin 的效果
    }
}

// 条件 Plugin:只在特定条件下激活
type ConditionalPlugin struct {
    cond  func(*Invocation) bool
    inner Plugin
}

func (c *ConditionalPlugin) BeforeRun(ctx context.Context, inv *Invocation) error {
    if !c.cond(inv) {
        return nil  // 条件不满足,跳过
    }
    return c.inner.BeforeRun(ctx, inv)
}

// 使用:只对支付相关 Agent 启用审计
auditPlugin := &ConditionalPlugin{
    cond:  func(inv *Invocation) bool { return inv.AgentInfo.Name == "payment_agent" },
    inner: NewAuditPlugin(auditStore),
}

Plugin 设计最佳实践:BeforeRun 做”检查和注入”(鉴权、限流、注入 trace),AfterRun 做”记录和清理”(计费、指标、资源释放),OnEvent 做”实时监听”(token 统计、进度上报)。Plugin 之间通过 State 传递数据,避免全局变量或闭包捕获。


2.6 Agent 实现类型矩阵

Agent 接口有多个实现,每种面向不同的使用场景:

实现类型 适用场景 Run 内部逻辑
LLMAgent 通用 Agent(最常用) 启动 LLM Flow 执行 ReAct 循环
GraphAgent 复杂工作流 启动 StateGraph 执行有向图
ChainAgent 线性流水线 按顺序调用多个子 Agent
ParallelAgent 并行执行 同时启动多个子 Agent,合并结果

关键设计:所有实现类型对外暴露的都是同一个 Agent 接口。Runner 不需要知道它调用的是哪种类型的 Agent——这就是接口的力量。你可以把一个 LLMAgent 替换为 GraphAgent,Runner 层的代码一行不改。


2.7 目录结构与代码量分布

trpc-agent-go/                       总计约 15000+ 行核心代码
├── agent/                           ~3000 行
│   ├── agent.go                     105 行 — Agent Interface
│   ├── llmagent/                    — LLMAgent 实现
│   ├── graphagent/                  — GraphAgent 实现
│   ├── parallelagent/               — ParallelAgent 实现
│   └── chainagent/                  — ChainAgent 实现
├── runner/                          ~4700 行
│   ├── runner.go                    3971 行 — Runner 核心
│   └── ralph_loop.go               732 行 — Ralph Loop 验证循环
├── internal/flow/llmflow/           ~2500 行
│   └── llmflow.go                   2468 行 — ReAct 循环核心
├── graph/                           ~6200 行
│   └── state_graph.go               6192 行 — StateGraph 图引擎
├── team/                            ~1400 行
│   ├── team.go                      553 行 — Team 入口
│   ├── runtime.go                   705 行 — Swarm 运行时
│   └── swarm.go                     144 行 — SwarmConfig
├── memory/                          — 记忆存储
├── session/                         — 会话管理
├── protocol/                        — 四大协议实现
│   ├── a2a/                         — Agent-to-Agent
│   ├── agui/                        — AG-UI
│   ├── mcp/                         — Model Context Protocol
│   └── openai/                      — OpenAI API 兼容
├── tool/                            — Tool 接口
├── model/                           — LLM 模型抽象
├── event/                           — 事件系统
├── planner/                         — 规划器
├── plugin/                          — 插件系统
└── observability/                   — 可观测性(OTel + Langfuse)

代码复杂度分布

StateGraph     ████████████████████████████████  6192 行(最复杂)
Runner         ████████████████████              3971 行
LLM Flow       ████████████                      2468 行
Invocation     ████████████                      2339 行(被 Runner 引用)
Team           ███████                           1402 行
Agent Interface █                                 105 行(最精简)

可以看到:接口越小,实现越重。105 行的 Agent Interface 定义了”做什么”,但”怎么做”分散在 6192 行(StateGraph)+ 3971 行(Runner)+ 2468 行(LLM Flow)中。这是好的软件设计——接口薄、实现厚。

2.7.5 与其他框架架构对比

vs LangGraph:图模型的两种路径

维度 tRPC-Agent-Go StateGraph LangGraph
语言 Go Python
状态表示 map[string]interface{} TypedDict + Channel + Reducer
类型安全 运行时检查(弱类型) 编译期类型提示 + Pydantic 校验
执行模型 goroutine + channel(并发) 同步超步 Pregel(串行推进)
流式输出 原生支持(Event Channel) stream_mode="values" 显式开启
Checkpoint 可选(Plugin 实现) 一等公民(MemorySaver / PostgresSaver)
人机交互 Plugin 拦截 + 手动实现 内建 interrupt_before / interrupt_after
子图嵌套 GraphAgent 作为 Team member 子图作为节点
代码量 ~6200 行(单文件) ~15000 行(多模块)

核心差异剖析

LangGraph 的 Reducer 机制——tRPC-Agent 没有对等物:

# LangGraph 的状态合并有严格的 Reducer 保证
class State(TypedDict):
    messages: Annotated[list, add]  # add reducer:只追加不覆盖
    # 节点A返回 {"messages": [msg1]} → state.messages = [..., msg1]
    # 节点B返回 {"messages": [msg2]} → state.messages = [..., msg1, msg2]

tRPC-Agent 的 State 是弱类型 map——后写覆盖前写,没有 Reducer 保证:

// tRPC-Agent 的状态合并:简单覆盖
state.Values["messages"] = append(state.Values["messages"].([]string), newMsg)
// 如果两个并行节点同时写同一个 key → 竞态条件!
// 需要开发者自己加锁或用不同的 key

这是 Go 的泛型表达力不足导致的设计取舍。好处是简单直接,坏处是缺乏编译期保证。

vs CrewAI:编排哲学的根本分歧

维度 tRPC-Agent-Go CrewAI
核心抽象 Agent Interface(5方法) Agent(role/goal/backstory)
编排方式 显式(代码写图/写链) 隐式(Process 自动调度)
状态管理 State + Session(精确控制) Task Output 传递(黑盒)
Agent间通信 Event Channel / State 共享 Task 的 context 字段
工具生态 需自行实现或接 MCP 继承 LangChain 工具
学习曲线 陡峭(需理解三层架构) 平缓(5分钟上手)
可调试性 高(每层有 trace/日志) 低(Process 内部是黑盒)
生产就绪 高(继承 tRPC 治理) 低(无内建治理能力)

关键洞察:CrewAI 选择”隐藏复杂性”——你不需要知道 Agent 之间怎么通信、状态怎么传递,框架帮你搞定。tRPC-Agent 选择”暴露复杂性”——你必须显式定义图的边、Plugin 的顺序、State 的读写。前者上手快但天花板低(遇到复杂场景无法精细控制),后者上手慢但天花板高(你能控制每一个细节)。

为什么选 Go?Go 在 Agent 领域的优劣

选 Go 的理由(对大厂微服务团队而言):

  1. 现有技术栈统一:团队后端服务已经是 Go,Agent 也用 Go 意味着同一套 CI/CD、同一套部署流程、同一套监控体系。用 Python 做 Agent 意味着要维护两套技术栈。
  2. 并发模型天然适合:goroutine 极其轻量(2KB 栈),channel 天然适合 Event Stream。Python 的 asyncio 在复杂场景下容易死锁。
  3. 编译型语言的部署优势:单二进制部署、无运行时依赖、启动速度极快(毫秒级)。Python 需要安装解释器 + 依赖 + 虚拟环境。
  4. 性能:Agent 本身的性能瓶颈在 LLM 调用(网络 IO),但在多 Agent 协调、事件路由、并发控制这些框架层逻辑中,Go 比 Python 快 10-100 倍。

Go 的劣势

  1. AI/ML 生态极不成熟:没有 NumPy / Pandas / scikit-learn / transformers 对等物。如果 Agent 需要做本地推理或数据处理,Go 几乎无解。
  2. 社区规模差距大:Python Agent 框架有数十万开发者贡献工具/教程/示例。Go 的 Agent 社区还在萌芽期。
  3. 泛型表达力不足:Go 1.18 才引入泛型,且功能有限。状态管理、事件类型等场景中缺乏类型安全保证(大量 interface{} 类型断言)。
  4. LLM SDK 可选项少:Python 有 OpenAI SDK / Anthropic SDK / LiteLLM 等成熟库。Go 的选项少且更新慢。

结论:如果你的团队是 Go 微服务团队、Agent 主要做”调度+编排”(调 LLM API + 调内部服务)而非”计算+推理”,选 Go 是正确的。如果你需要大量 AI/ML 计算或想快速原型,Python 是唯一选择。


2.8 与 tRPC 微服务框架的集成点

tRPC-Agent-Go 不是一个独立的库——它寄生在 tRPC 框架之上。具体集成点:

tRPC 能力 Agent 框架如何利用
服务注册/发现 Agent 注册为一个 tRPC 服务,其他 Agent 通过服务发现找到它
负载均衡 多实例部署的同一个 Agent,请求自动分配到不同实例
熔断限流 Agent 调用外部 LLM 出错时自动熔断,防止雪崩
链路追踪 每次 Agent 调用自动生成 trace span,与微服务 trace 打通
配置中心 Agent 的 prompt/model/max_steps 可以热更新
监控告警 Agent 的 token 消耗、响应延迟、错误率实时可见

核心价值:这些能力在其他 Agent 框架中要么没有(CrewAI/Agno),要么需要额外购买商业服务(LangChain → LangSmith),要么需要自己搭建。tRPC-Agent 直接继承——零开发成本获得十年微服务治理经验

2.8.5 部署架构与生产实践

单 Agent 部署

最简单的场景:一个 Agent 服务,背后调 LLM API。

                    ┌─────────────┐
用户请求 ──────────→│  Agent Pod  │──────→ LLM API (OpenAI/自部署)
    ↑               │ (Runner +   │──────→ 工具服务 (搜索/DB)
    │               │  LLMAgent)  │
    │               └─────────────┘
    │                      │
    └──────────────────────┘ Event Stream (SSE/WebSocket)

部署要点:

# Kubernetes Deployment 示例
apiVersion: apps/v1
kind: Deployment
metadata:
  name: research-agent
spec:
  replicas: 3  # 多副本 → tRPC 负载均衡自动分配请求
  template:
    spec:
      containers:
      - name: agent
        image: registry/research-agent:v1.2
        resources:
          requests:
            memory: "256Mi"   # Agent 本身很轻量(Go 二进制)
            cpu: "100m"       # CPU 主要消耗在序列化/反序列化
          limits:
            memory: "512Mi"
            cpu: "500m"
        env:
        - name: LLM_API_KEY
          valueFrom:
            secretKeyRef:
              name: llm-secrets
              key: api-key
        - name: MAX_CONCURRENT_REQUESTS
          value: "50"         # 单实例最大并发(受 LLM API rate limit 约束)

多 Agent 部署:微服务拓扑

复杂场景:多个 Agent 各自独立部署,通过 tRPC 服务发现互相调用。

┌──────────────────────────────────────────────────┐
│                    API Gateway                     │
└──────────────┬───────────────────┬───────────────┘
               │                   │
    ┌──────────▼──────────┐   ┌───▼───────────────┐
    │  Coordinator Agent  │   │  Direct Agent API  │
    │  (路由 + 调度)       │   │  (简单查询直接走)   │
    └──┬──────────────┬───┘   └───────────────────┘
       │              │
  ┌────▼────┐    ┌───▼─────┐
  │Research │    │ Writer  │   ← 每个 Agent 独立部署
  │Agent ×3 │    │Agent ×2 │   ← 可以独立扩缩容
  └────┬────┘    └────┬────┘
       │              │
  ┌────▼────┐    ┌───▼─────┐
  │搜索服务  │    │文档服务  │   ← Agent 依赖的业务服务
  └─────────┘    └─────────┘

多 Agent 部署的关键决策

决策点 选项A 选项B 推荐
Agent 粒度 一个服务包含多个 Agent 每个 Agent 一个服务 初期A,规模大了B
通信方式 进程内直接调用 RPC 跨进程调用 Team内A,Team间B
状态存储 本地内存 Redis/DB 多副本必须用外部存储
Agent 版本 统一版本 各自独立版本 B(独立迭代)

灰度发布:Agent 的特殊考量

Agent 的灰度发布比普通微服务更复杂——因为 Agent 的行为取决于 prompt 和 model,而不只是代码:

// tRPC 配置中心支持热更新 Agent 配置
// 不需要重新部署就能切换 prompt / model
type AgentConfig struct {
    Model       string  `yaml:"model"`        // gpt-4o → claude-3.5-sonnet
    SystemPrompt string `yaml:"system_prompt"` // prompt 版本切换
    MaxSteps    int     `yaml:"max_steps"`
    Temperature float64 `yaml:"temperature"`
}

// 灰度策略:按用户ID hash 分流
func (r *Runner) selectAgentVersion(userID string) *AgentConfig {
    hash := fnv32(userID) % 100
    if hash < 10 {
        return r.configs["canary"]   // 10% 用户走新版本
    }
    return r.configs["stable"]        // 90% 用户走稳定版
}

Agent 灰度发布的三个层次

  1. 代码灰度(传统):新版本 Agent 代码部署到部分实例
  2. Prompt 灰度:同一份代码,部分用户用新 prompt(通过配置中心热更新)
  3. Model 灰度:同一份代码同一个 prompt,部分用户用新模型(GPT-4o → Claude 3.5)

三层灰度可以组合使用——这是传统微服务灰度不需要考虑的 Agent 特有问题。


2.9 设计哲学总结

tRPC-Agent-Go 的设计可以用三个词概括:

  1. 极简接口:5 个方法定义 Agent 的全部能力边界
  2. 流式异步:Event Channel 解耦执行与消费
  3. 微服务共生:不重新发明轮子,复用已有基础设施

它做了一个”反常识”的选择:别的框架在应用层解决的问题(服务发现、负载均衡、可观测性),它推给了基础设施层。这减少了框架自身的代码量(~100 核心文件 vs LangChain 的 15000+),代价是你必须在 tRPC 生态里才能享受这些好处。


思考题

Q1(架构理解):Runner 有 3971 行,LLM Flow 有 2468 行。为什么要分两层而不是合并成一个?如果合并,会有什么问题?(提示:考虑 GraphAgent 和 ChainAgent 需不需要 LLM Flow)

Q2(设计决策)Agent.Run() 返回 <-chan *event.Event 而不是直接返回 string。这带来了什么好处?如果某天需要实现”用户发消息中途打断 Agent”的功能,这个设计能支持吗?怎么实现?

Q3(接口设计):Agent Interface 有 5 个方法。如果让你从头设计,你会去掉哪个方法?为什么?或者你觉得需要加一个什么方法?

Q4(集成思考):假设一个公司的后端全是 Spring Boot(Java),他们想用 tRPC-Agent-Go 构建 Agent。这可行吗?微服务治理的优势还在吗?(提示:想想服务注册发现和 RPC 协议的兼容性)

Q5(代码阅读计划):如果你只有一周时间通读 tRPC-Agent-Go 的核心代码,你会按什么顺序读?(提示:从最小的开始,逐层向外扩展)


上一章:第一章 AI Agent 框架总论与四大编排范式

下一章:第三章 ReAct 循环与 LLM Flow 深度解析

返回目录:Agent 框架深度导读系列