写在前面
白泽是一个 "旁挂式" 的 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)
│
<span>[ HITL 审批门 ]</span>
│
┌─────────────┴─────────────┐
直接执行(插件 / 代理) HTTP 回调执行器(回调企业侧地址)
- 核心循环(
internal/run):LLM 思考 → 选择工具 → 执行 → 汇报。事件流(llm.thinking、llm.tool_call、tool.result)全程落库,操作台可以边跑边看。 - 工具路由器(
internal/tool):一个带锁的 map,注册的不是 "函数",而是 "工具契约 + 执行闭包"。 - 执行器(
internal/connector):工具从三种来源进入 ——OpenAPI 文档、HTTP 插件、MCP 工具服务;其中还有一种 "回调执行" 模式,把执行权交回企业侧。
三、关键设计决策
1. 为什么用 HTTP 回调,而不是插件协议
这是白泽最核心的一个取舍。
插件协议的问题:进程内加载插件(Go plugin、共享库、语言绑定 SDK)要求插件能和宿主进程编译到一起 —— 语言、版本、ABI 全要对齐。而企业里的系统大多数不是 Go 写的:遗留系统、Java/.NET/Python 服务,进程内插件根本加载不进去。就算加载进去了,升级插件等于重启进程,"旁挂即用、停用干净" 就没了。
HTTP 回调的做法:白泽不直接执行工具,而是把调用信息 POST 到企业自己的地址:
<span>{</span>
<span>"tool"</span><span>:</span> <span>"create_ticket"</span><span>,</span>
<span>"arguments"</span><span>:</span> <span>{</span> ... <span>}</span><span>,</span>
<span>"run_id"</span><span>:</span> <span>"run_xxx"</span><span>,</span>
<span>"agent_id"</span><span>:</span> <span>"agent_xxx"</span><span>,</span>
<span>"idempotency_key"</span><span>:</span> <span>"uuid-xxx"</span><span>,</span>
<span>"callback_urls"</span><span>:</span> <span>{</span> <span>"event"</span><span>:</span> <span>"https://your-service/baize-events"</span> <span>}</span>
<span>}</span>
由企业侧执行,再把结果回传。好处是:语言无关、进程隔离、可审计;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_call→tool.result的事件流落库,出问题可以回溯到每一步; - 上下文压缩:长会话自动做滚动摘要,避免上下文爆炸导致质量劣化。
四、与主流方案的对比(Go vs Python / Node.js)
先承认事实:Python 在 AI/Agent 生态上是最好的选择。LangChain、LlamaIndex 这类框架都在 Python 里,模型推理的参考实现也几乎都是 Python。如果目标是快速验证想法、深度复用 LLM 生态,Python 没有对手。
白泽选 Go,是因为它的定位不同:
| 维度 | Go | Python | Node.js |
|---|---|---|---|
| 部署交付 | 单二进制、零依赖 | 解释器 + 依赖安装 / 虚拟环境 | Node 运行时 + node\_modules |
| 资源占用 | 低,一个进程常驻无压力 | 偏高,常驻需要额外治理 | 中等 |
| 并发模型 | goroutine 原生并发 | GIL 受限,靠多进程 / 异步 | 事件循环 |
| 类型安全 | 静态类型,编译期检查 | 动态类型,运行时才发现 | 动态 / TypeScript |
| LLM 生态 | 较新,但在快速补齐 | 最丰富 | 丰富 |
| 跨平台 | 交叉编译一键出全平台 | 目标机需装解释器 | 目标机需装 Node |
结论不是 "Go 比 Python 好",而是定位决定语言:
- 目标是 "框架 / 快速实验"→ Python;
- 目标是 "要旁挂、要常驻、要一键部署到企业环境、要在低配机器上长期运行"→ Go 在部署和资源占用上的优势是不可替代的。
五、核心代码片段(Go 实现)
以下代码均来自项目源码,做了精简。每段配一句 "这段在解决什么"。
1. 工具 = 契约 + 执行闭包
把 "工具" 建模成 "给模型看的契约(Spec)+ 由连接器注入的执行闭包(Invoker)",路由和执行完全解耦:
<span>type</span> Invoker <span><span>func</span><span>(ctx context.Context, args <span>map</span>[<span>string</span>]any)</span></span> (
content <span>map</span>[<span>string</span>]any, isError <span>bool</span>, err <span>error</span>)
<span>type</span> Meta <span>struct</span> {
Spec llm.ToolSpec
ConnectorID <span>string</span>
Method <span>string</span>
Path <span>string</span>
RequireLogin <span>bool</span>
SecuritySchemes []<span>string</span>
}
2. 运行时动态注册(安全策略随条目固化)
注册时就把 require_approval / require_login 写进条目,工具列表是 "热" 的,加 / 停连接器都不用重启:
<span>func</span> <span>(r</span> <span>*Registry)</span> <span>RegisterMeta(meta</span> <span>Meta,</span> <span>inv</span> <span>Invoker,</span> <span>requireApproval</span> <span>bool)</span> {
<span>r.mu.Lock()</span>
<span>defer</span> <span>r.mu.Unlock()</span>
<span>r.tools</span>[<span>meta.Spec.Name</span>] <span>=</span> <span>entry</span>{
<span>spec:</span> <span>meta.Spec</span>,
<span>invoker:</span> <span>inv</span>,
<span>requireApproval:</span> <span>requireApproval</span>,
<span>requireLogin:</span> <span>meta.RequireLogin</span>,
<span>connectorID:</span> <span>meta.ConnectorID</span>,
<span>method:</span> <span>meta.Method</span>,
<span>path:</span> <span>meta.Path</span>,
}
}
3. HTTP 回调执行器
把 "执行权" 交回企业侧;幂等键保证网络重试不会重复执行:
payload := <span>map</span>[<span>string</span>]any{
<span>"tool"</span>: tool,
<span>"arguments"</span>: args,
<span>"run_id"</span>: meta.RunID,
<span>"agent_id"</span>: meta.AgentID,
<span>"idempotency_key"</span>: meta.IdempotencyKey,
}
<span>if</span> strings.TrimSpace(meta.CallbackEventURL) != <span>""</span> {
payload[<span>"callback_urls"</span>] = <span>map</span>[<span>string</span>]any{
<span>"event"</span>: 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) && <span>t.Source</span> == store.ToolSourceSpec {
<span>needApproval</span> = <span>true</span>
}
5. 超时与失败降级
超时兜底 + "失败即内容" 的语义,让一次工具失败不会炸掉整个会话:
toolCtx, toolCancel := context.WithTimeout(ctx, e.toolTimeout())
<span>defer</span> toolCancel()
content, isError, invErr := e.Tools.Invoke(toolCtx, payload.ToolName, payload.Arguments)
<span>if</span> invErr != <span>nil</span> {
<span>// 基础设施故障(超时/网络):落库并结束本轮</span>
<span>return</span> e.finalizeFailedRun(runID, invErr)
}
<span>// isError=true 时:失败作为内容回传模型,由模型决定重试或解释</span>
结尾
白泽还在早期阶段,上面这些取舍远没有到 "最优" 的程度,尤其是审批体验、渠道适配、执行器扩展这几个方向,欢迎有真实场景的人来拍砖。
- 仓库:github.com/rebornace/b…(MIT)
- 国内镜像:gitee.com/RebornAce/b…
- Issues 里聊聊你的场景和想法,我会持续跟进。
需要旁挂常驻、一键交付企业内网、低配机器长期运行的团队,可重点参考其 HTTP 回调与热注册设计;若追求快速验证、复用 LLM 生态,Python 仍更划算。