第二章: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 之间的中间人。它负责:
- Session 管理:同一个用户的多轮对话归属同一个 session
- 请求生命周期:生成 RequestID、注册/取消/查询运行状态
- 事件循环:持续监听 Agent 产出的事件流,做后处理
- 消息持久化:把每轮对话存入 session storage
- Agent 选择:多 Agent 场景下选择哪个 Agent 执行
Runner 不负责:具体的 LLM 调用逻辑、工具执行逻辑、思考策略。
日常类比:餐厅前台。她负责接待客人(接收请求)、分配座位(选择 Agent)、记录点单(session 持久化)、确认是否取消(Cancel)、通知菜好了(事件分发)。但她不做菜。
LLM Flow 层(2468 行)—— “厨师的工作循环”
LLM Flow 是真正的 ReAct 循环执行者。它负责:
- 构建 LLM 请求:把 session 历史、系统指令、可用工具注入请求
- 调用 LLM:发送请求,处理流式响应
- 工具执行:如果 LLM 返回 tool_call,执行对应工具
- 循环控制:决定是否继续下一轮(还有 tool_call?Agent 说结束了?超过最大步数?)
- 事件发射:每一步产出对应的事件
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 对象深度解析
State 是 Invocation 中最容易被忽略、但最重要的字段之一。它是节点之间传递自定义数据的载体。
// 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
}
为什么这样设计?三个原因:
- 非阻塞:调用方不需要等 Agent 执行完。一个 Agent 可能思考 30 秒甚至更久——如果用同步调用,整个系统就卡住了。
- 流式输出:用户可以实时看到 Agent 的思考过程(正在搜索…正在写…),不需要等最终结果出来才有反馈。
- 可组合: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 的理由(对大厂微服务团队而言):
- 现有技术栈统一:团队后端服务已经是 Go,Agent 也用 Go 意味着同一套 CI/CD、同一套部署流程、同一套监控体系。用 Python 做 Agent 意味着要维护两套技术栈。
- 并发模型天然适合:goroutine 极其轻量(2KB 栈),channel 天然适合 Event Stream。Python 的 asyncio 在复杂场景下容易死锁。
- 编译型语言的部署优势:单二进制部署、无运行时依赖、启动速度极快(毫秒级)。Python 需要安装解释器 + 依赖 + 虚拟环境。
- 性能:Agent 本身的性能瓶颈在 LLM 调用(网络 IO),但在多 Agent 协调、事件路由、并发控制这些框架层逻辑中,Go 比 Python 快 10-100 倍。
Go 的劣势:
- AI/ML 生态极不成熟:没有 NumPy / Pandas / scikit-learn / transformers 对等物。如果 Agent 需要做本地推理或数据处理,Go 几乎无解。
- 社区规模差距大:Python Agent 框架有数十万开发者贡献工具/教程/示例。Go 的 Agent 社区还在萌芽期。
- 泛型表达力不足:Go 1.18 才引入泛型,且功能有限。状态管理、事件类型等场景中缺乏类型安全保证(大量
interface{}类型断言)。 - 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 灰度发布的三个层次:
- 代码灰度(传统):新版本 Agent 代码部署到部分实例
- Prompt 灰度:同一份代码,部分用户用新 prompt(通过配置中心热更新)
- Model 灰度:同一份代码同一个 prompt,部分用户用新模型(GPT-4o → Claude 3.5)
三层灰度可以组合使用——这是传统微服务灰度不需要考虑的 Agent 特有问题。
2.9 设计哲学总结
tRPC-Agent-Go 的设计可以用三个词概括:
- 极简接口:5 个方法定义 Agent 的全部能力边界
- 流式异步:Event Channel 解耦执行与消费
- 微服务共生:不重新发明轮子,复用已有基础设施
它做了一个”反常识”的选择:别的框架在应用层解决的问题(服务发现、负载均衡、可观测性),它推给了基础设施层。这减少了框架自身的代码量(~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 的核心代码,你会按什么顺序读?(提示:从最小的开始,逐层向外扩展)
下一章:第三章 ReAct 循环与 LLM Flow 深度解析
返回目录:Agent 框架深度导读系列