一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

Go 轻量级 AI Agent 实践:拆解白泽的架构与工程取舍

时间:2026-09-17 12:16:01 编辑:袖梨 来源:一聚教程网

将 AI Agent 接入企业系统,难点往往不在模型调用,而在部署成本、工具执行边界与写操作安全。白泽选择以独立 Go 进程运行,通过统一注册表连接多类工具,并用 HTTP 回调把实际执行交还业务系统。下面从这一定位出发,拆解它的分层架构、审批机制和失败处理策略。


写在前面

白泽是一个 "旁挂式" 的 AI 助手运行时:单独一个进程,配置放在应用外面,把接口文档变成助手可调用的工具,重要写操作先请人批准,停掉之后业务侧几乎不留痕迹。

一句话定位:它是运行时,不是框架。 这个定位决定了下面所有的架构选择。


一、为什么选 Go

1. 单一二进制、零依赖

旁挂式部署的核心诉求是 "一个进程,拷过去就能跑"。Go 编译出的单二进制文件,不需要目标机器上有解释器、不需要装依赖、不需要虚拟环境。这对要常驻在企业环境里的助手来说,是交付成本最低的形态。

2. goroutine 并发模型

一个助手进程要同时服务多个入口:操作台、带签名的告警 / 工单来信、即时消息渠道。goroutine 让 "一个进程并行处理多个会话" 变得非常自然;工具调用之间的并行(多工具同时执行)在 Go 里也几乎是顺手的事。

3. 跨平台交叉编译

企业环境什么平台都有:Windows、Linux、macOS、ARM。Go 一行 GOOS=linux GOARCH=arm64 go build 就能出目标平台的二进制,不用在目标机器上搭环境。

4. 静态类型 + 工具契约

工具的输入 schema 来自 OpenAPI 文档,映射到 Go 的强类型结构后,很多错误在编译期就被拦下来了。对一个要长时间运行的守护进程来说,这比动态语言省心得多。


二、架构总览

整体是一个清晰的 "核心循环 → 工具路由器 → 执行器" 三层结构:

用户 / 渠道 ──► Agent 核心循环(思考 → 选工具 → 执行 → 汇报)
                        │
                        ▼
                 工具路由器(Registry)
                        │
       ┌────────────────┼────────────────┐
  OpenAPI 连接器      HTTP 插件        MCP 连接器
       └────────────────┼────────────────┘
                        ▼
             Invoker 执行闭包(注册进 Registry)
                        │
                   [ HITL 审批门 ]
                        │
          ┌─────────────┴─────────────┐
     直接执行(插件 / 代理)      HTTP 回调执行器(回调企业侧地址)
  • 核心循环internal/run):LLM 思考 → 选择工具 → 执行 → 汇报。事件流(llm.thinkingllm.tool_calltool.result)全程落库,操作台可以边跑边看。

  • 工具路由器internal/tool):一个带锁的 map,注册的不是 "函数",而是 "工具契约 + 执行闭包"。

  • 执行器internal/connector):工具从三种来源进入 ——OpenAPI 文档、HTTP 插件、MCP 工具服务;其中还有一种 "回调执行" 模式,把执行权交回企业侧。


三、关键设计决策

1. 为什么用 HTTP 回调,而不是插件协议

这是白泽最核心的一个取舍。

插件协议的问题:进程内加载插件(Go plugin、共享库、语言绑定 SDK)要求插件能和宿主进程编译到一起 —— 语言、版本、ABI 全要对齐。而企业里的系统大多数不是 Go 写的:遗留系统、Java/.NET/Python 服务,进程内插件根本加载不进去。就算加载进去了,升级插件等于重启进程,"旁挂即用、停用干净" 就没了。

HTTP 回调的做法:白泽不直接执行工具,而是把调用信息 POST 到企业自己的地址:

{
  "tool": "create_ticket",
  "arguments": { ... },
  "run_id": "run_xxx",
  "agent_id": "agent_xxx",
  "idempotency_key": "uuid-xxx",
  "callback_urls": { "event": "https://your-service/baize-events" }
}

由企业侧执行,再把结果回传。好处是:语言无关、进程隔离、可审计;idempotency_key 幂等键保证网络重试不会重复执行;callback_urls 让企业侧可以继续推进后续动作。

代价:多一次网络往返;回调地址必须可达;为了防止有人伪造回调,需要签名鉴权(白泽用回调签名 + TTL 防重放)。

2. 如何实现工具的动态注册与发现

工具注册表(tool.Registry)是核心数据结构:sync.RWMutex 保护一个 map,支持运行时的注册、注销、按连接器批量注销 —— 加一个工具、停一个连接器都不用重启进程。

三种工具来源走同一个注册通道:

  • OpenAPI 文档:导入 Swagger/OpenAPI/Postman 文档,每个 operation 变成一个工具;

  • HTTP 插件:一个旁路小服务,按约定声明 "有哪些工具、怎么执行";

  • MCP 工具服务:作为 MCP 客户端连接外部工具生态。

注册时就把安全策略固化进条目:require_approval(需要人批准)、require_login(需要会话登录)、security_schemes(用哪个鉴权方案)。安全策略在注册期决定,而不是执行时临时问—— 这是白泽敢让助手 "干活" 的前提。

工具的发现也很简单:Registry.List() / Registry.Specs() 输出给模型当工具列表,操作台实时可见。

3. 如何保证调用失败时的优雅降级

AI Agent 的失败是常态,所以降级设计比成功路径更重要:

  • 超时兜底:每次工具调用都挂在 context.WithTimeout 上,默认 60 秒,可配置;

  • 失败也是 "内容"Invoker 返回 (content, isError, err) 三值 ——err 是基础设施故障(超时、网络断了),isError 是业务侧失败。两者都作为结构化内容回传给模型,模型可以选择重试、换工具,或者向用户解释,而不是中断整个会话;

  • 审批拒绝不是崩溃:写操作被人在操作台驳回后,run 进入明确的 "rejected" 终态,事件留痕,而不是抛异常;

  • 全程可观测llm.tool_calltool.result 的事件流落库,出问题可以回溯到每一步;

  • 上下文压缩:长会话自动做滚动摘要,避免上下文爆炸导致质量劣化。


四、与主流方案的对比(Go vs Python / Node.js)

先承认事实:Python 在 AI/Agent 生态上是最好的选择。LangChain、LlamaIndex 这类框架都在 Python 里,模型推理的参考实现也几乎都是 Python。如果目标是快速验证想法、深度复用 LLM 生态,Python 没有对手。

白泽选 Go,是因为它的定位不同:

维度GoPythonNode.js
部署交付单二进制、零依赖解释器 + 依赖安装 / 虚拟环境Node 运行时 + node_modules
资源占用低,一个进程常驻无压力偏高,常驻需要额外治理中等
并发模型goroutine 原生并发GIL 受限,靠多进程 / 异步事件循环
类型安全静态类型,编译期检查动态类型,运行时才发现动态 / TypeScript
LLM 生态较新,但在快速补齐最丰富丰富
跨平台交叉编译一键出全平台目标机需装解释器目标机需装 Node

结论不是 "Go 比 Python 好",而是定位决定语言

  • 目标是 "框架 / 快速实验"→ Python;

  • 目标是 "要旁挂、要常驻、要一键部署到企业环境、要在低配机器上长期运行"→ Go 在部署和资源占用上的优势是不可替代的。


五、核心代码片段(Go 实现)

以下代码均来自项目源码,做了精简。每段配一句 "这段在解决什么"。

1. 工具 = 契约 + 执行闭包

把 "工具" 建模成 "给模型看的契约(Spec)+ 由连接器注入的执行闭包(Invoker)",路由和执行完全解耦:

type Invoker func(ctx context.Context, args map[string]any) (
    content map[string]any, isError bool, err error)
type Meta struct {
    Spec            llm.ToolSpec
    ConnectorID     string
    Method          string
    Path            string
    RequireLogin    bool
    SecuritySchemes []string
}

2. 运行时动态注册(安全策略随条目固化)

注册时就把 require_approval / require_login 写进条目,工具列表是 "热" 的,加 / 停连接器都不用重启:

func (r *Registry) RegisterMeta(meta Meta, inv Invoker, requireApproval bool) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.tools[meta.Spec.Name] = entry{
        spec:            meta.Spec,
        invoker:         inv,
        requireApproval: requireApproval,
        requireLogin:    meta.RequireLogin,
        connectorID:     meta.ConnectorID,
        method:          meta.Method,
        path:            meta.Path,
    }
}

3. HTTP 回调执行器

把 "执行权" 交回企业侧;幂等键保证网络重试不会重复执行:

payload := map[string]any{
    "tool":            tool,
    "arguments":       args,
    "run_id":          meta.RunID,
    "agent_id":        meta.AgentID,
    "idempotency_key": meta.IdempotencyKey,
}
if strings.TrimSpace(meta.CallbackEventURL) != "" {
    payload["callback_urls"] = map[string]any{
        "event": meta.CallbackEventURL,
    }
}
rawPayload, _ := json.Marshal(payload)
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.URL, bytes.NewReader(rawPayload))

4. 写操作自动进审批门

非 GET/HEAD/OPTIONS 的写操作在注册期自动标记 "需审批",由人在操作台点批准 / 驳回后才执行:

needApproval := t.RequireApproval
if ctx.requireApprovalMutating && isMutatingMethod(t.Method) && t.Source == store.ToolSourceSpec {
    needApproval = true
}

5. 超时与失败降级

超时兜底 + "失败即内容" 的语义,让一次工具失败不会炸掉整个会话:

toolCtx, toolCancel := context.WithTimeout(ctx, e.toolTimeout())
defer toolCancel()
content, isError, invErr := e.Tools.Invoke(toolCtx, payload.ToolName, payload.Arguments)
if invErr != nil {
    // 基础设施故障(超时/网络):落库并结束本轮
    return e.finalizeFailedRun(runID, invErr)
}
// isError=true 时:失败作为内容回传模型,由模型决定重试或解释

结尾

白泽还在早期阶段,上面这些取舍远没有到 "最优" 的程度,尤其是审批体验、渠道适配、执行器扩展这几个方向,欢迎有真实场景的人来拍砖。

  • 仓库:github.com/rebornace/b…(MIT)

  • 国内镜像:gitee.com/RebornAce/b…

  • Issues 里聊聊你的场景和想法,我会持续跟进。

热门栏目