Go 基础体系 · 第 102/113 篇。示例统一基于 Go 1.26.4;核心片段可能省略 package 与 import,完整程序可直接按文中结构运行。

Go AI 工程学习路线:从模型 API、RAG 到 Agent 与生产治理

本文以 Go 1.26.4 为语言基准,示例平台固定 OpenAI Responses API(2026-08 稳定接口)、github.com/openai/openai-go/v3 v3.9.0、PostgreSQL 17.6、pgvector 0.8.0、Redis 8.2 和 OpenTelemetry Go v1.38.0。模型使用配置中的不可变 snapshot/version ID;文中 gpt-5-mini 仅作已部署别名示例,生产发布必须记录实际解析的模型、参数和提供商区域。框架可选 CloudWeGo Eino v0.5.0,但学习前半程先用标准 HTTP、JSON、SSE 和数据库看清协议,不让编排框架隐藏取消、重试与费用。

Go 适合 AI 应用的 API 网关、流式连接、检索、工具执行、队列和控制面,却不负责训练大型基础模型的主流 GPU 内核。模型输出具有概率性,可能编造事实、拒答、格式漂移、超时或被提示注入。工程目标不是让一次演示“看起来聪明”,而是让质量、权限、成本、延迟和失败都可测、可限制、可恢复。

1. 先建立完整系统边界

一个生产 AI 应用至少包含入口、上下文构建、模型调用、检索、工具、副作用确认、评测、观测与数据治理。模型是其中一个不可靠外部服务,不是业务数据库,也不是授权系统。

用户 -> 认证/配额 -> 输入策略 -> 会话/上下文
                                |-> Retriever -> 权限过滤 -> 引用
                                |-> Model -> stream/structured output
                                `-> Tool loop -> schema -> authz -> business API
结果 -> 输出策略 -> 审计/成本/Trace -> 用户

先写“不由模型决定”的清单:用户身份、租户权限、价格、余额、库存、审批、删除和支付都由确定性代码与数据库决定。模型只能提出候选参数或解释结果。外链、模型知识和向量相似度都不能替代权威业务状态。

2. 路线总览:用六个可验收项目递进

第一阶段做一个带 deadline 的非流式摘要 API,掌握 HTTP、token、错误和费用。第二阶段加入 SSE 流式输出与取消。第三阶段用 JSON Schema 做结构化抽取。第四阶段构建带引用和权限过滤的 RAG。第五阶段实现有白名单、步数上限和幂等工具的 Agent loop。第六阶段补齐离线评测、红队、监控、灰度与故障恢复。

每阶段都有退出标准,而不是“看完教程”:固定测试集质量达到阈值;P95/P99 与单请求成本可报告;取消后连接和 goroutine 退出;429/5xx/半流失败有明确结果;安全用例不能越权;模型/提示/索引变更可回滚。前一阶段未达标,不增加多 Agent、长期记忆等复杂度。

3. 第一阶段:理解模型请求与响应契约

先直接调用 Responses API。请求包括 model、input、最大输出、工具和响应格式;响应包含 output item、usage、状态、错误和 request ID。不要假设文本总在数组固定位置,按 SDK 的 typed item 或协议类型遍历。

func summarize(
	ctx context.Context,
	client *openai.Client,
	article string,
) (string, error) {
	requestCtx, cancel := context.WithTimeout(ctx, 12*time.Second)
	defer cancel()

	response, err := client.Responses.New(requestCtx, responses.ResponseNewParams{
		Model: "gpt-5-mini",
		Input: responses.ResponseNewParamsInputUnion{
			OfString: openai.String("请用三点概括以下内容:\n" + article),
		},
		MaxOutputTokens: openai.Int(400),
	})
	if err != nil {
		return "", fmt.Errorf("create model response: %w", err)
	}
	text := response.OutputText()
	if text == "" {
		return "", errors.New("model response contained no text")
	}
	return text, nil
}

SDK 类型会随稳定大版本演进,升级时按锁定版本编译契约测试。输入有字符与 token 双上限,输出设硬上限。请求 context 从 HTTP 入口传递,用户断开即取消下游;后台批处理使用任务生命周期 context,不能保存已结束请求的 context。

4. 错误分类、重试与未知结果

至少区分本地校验、认证/授权、配额/429、服务端 5xx、网络、deadline、内容拒绝和响应协议错误。认证错误不能重试;429/5xx 可按 Retry-After 和总预算重试;context canceled 立即停止。SDK 已重试时,业务层不要再无界叠加。

非流式生成通常没有外部业务副作用,响应丢失可用相同业务请求重新生成,但文本可能不同。若生成结果被收费、持久化或触发工具,创建 generation_id 与 idempotency key,保存请求哈希、模型 snapshot、状态和 provider request ID。超时表示未知,先查本地状态或提供商查询能力。

指数退避必须有 full jitter、最大次数和总时间。重试会增加费用和尾延迟,指标单独记录 attempts。模型上下文过长是确定性错误,应裁剪或拒绝,而不是重试。

5. 第二阶段:SSE 流式输出与取消

流式接口通常以 Server-Sent Events 发送增量 item、完成 usage 和错误。SSE 是 UTF-8 文本帧,事件由空行分隔,每行可有 event:id:data:data 可多行。不要用 bufio.Scanner 默认 64 KiB 限制而不调整,也不要按 TCP read 边界解析。

event: response.output_text.delta
data: {"response_id":"resp_42","delta":"你好"}

event: response.completed
data: {"response_id":"resp_42","usage":{"input_tokens":120,"output_tokens":31}}

服务端代理流时先验证用户与输入,再提交 Content-Type: text/event-stream,每个业务事件编码成自己的稳定 schema 并 Flush。上游增量不是可直接拼接的任意字符串:工具参数、引用和文本是不同 item。客户端断开后请求 context 取消,上游 reader 必须关闭,生产 goroutine 必须被等待。

一旦已向客户端发送 200,后续失败不能改成 HTTP 500,只能发协议内 error event 或断流。产品要定义部分文本是草稿、可保留结果还是必须丢弃;数据库不要每 token 写一次,按有界时间/字节批量 checkpoint。

7. 第三阶段:结构化输出与 Schema

自然语言解析不适合订单字段、标签和工具参数。使用 JSON Schema 约束形状,仍要在 Go 中严格解码、禁止未知字段、校验枚举、长度与跨字段规则。Schema 让格式更可靠,不保证事实正确。

把 schema、system instruction、few-shot 示例和解析器作为同一版本发布。结构变更先让消费者兼容新旧,再切模型输出。验证失败可在预算内进行一次“带错误反馈的修复”,但要统计修复率;长期高修复率说明 schema/模型不匹配,不应用重试掩盖。

confidence 是模型自报分数,不是校准概率。需要在标注集上画可靠性曲线,按业务成本选择自动执行、人工复核和拒绝阈值。

9. 第四阶段:RAG 数据管线

RAG 不只是向量搜索。离线管线读取文档、解析、规范化、切块、生成 embedding、写索引;在线管线把问题转换、召回、权限过滤、重排并构造带引用上下文。原文对象存储/数据库是事实源,向量库是可重建索引。

source(version, ACL) -> parse -> chunks(chunk_id, offsets, text_hash)
                     -> embedding(model_version, vector) -> index

query(user ACL) -> candidate recall -> ACL filter -> rerank -> context -> model

每个 chunk 保存 document ID、source version、字符/页码范围、租户、ACL 标签、文本哈希、embedding model 和索引版本。更新文档先构建新版本,完成后原子切换索引指针;删除权限要快速生效,不能等待批量重建。Embedding 维度和距离度量是 schema,换模型通常要新索引。

切块按文档结构和任务评测,不机械固定字符数。过小丢上下文,过大降低召回并吃 token。表格、代码和扫描 PDF 需要专门解析,解析失败进入可观察状态而不是索引乱码。

10. 检索、权限过滤与引用

向量相似适合语义召回,BM25 适合精确词,混合召回再重排通常更稳。top-k 不是越大越好,候选增加会提高 token、延迟和噪声。离线用 Recall@k、MRR/nDCG 和“答案所需证据是否出现”评估检索;生成答案另测忠实度。

权限过滤必须在返回 chunk 给模型前执行,并尽可能下推到查询。先跨租户召回再让模型“不要泄漏”已经越界;trace 和缓存也可能保存秘密。缓存 key 包含租户、权限版本、query 规范化、索引版本和检索配置。

引用由系统根据选中的 chunk metadata 生成,模型只选择 citation ID,不能让它自由编 URL。响应后校验每个引用属于本次授权候选、引用文本确实支持相邻主张。没有证据时明确“不知道”,而不是用参数温度掩盖。

12. 第五阶段:工具调用不是函数自动执行

模型返回工具名和 JSON 参数只是未信任提议。服务端按白名单查工具,严格 schema 解码,再基于真实用户/租户授权,设置 deadline 与幂等键后调用。工具结果也不可信,网页、邮件或工单可能包含提示注入;返回模型前裁剪、标记来源并移除秘密。

type Tool interface {
	Name() string
	Execute(ctx context.Context, principal Principal, arguments json.RawMessage) (ToolResult, error)
}

func executeTool(
	ctx context.Context,
	registry map[string]Tool,
	principal Principal,
	call ToolCall,
) (ToolResult, error) {
	tool, ok := registry[call.Name]
	if !ok {
		return ToolResult{}, fmt.Errorf("tool %q is not allowed", call.Name)
	}
	if err := authorize(principal, call.Name); err != nil {
		return ToolResult{}, fmt.Errorf("authorize tool %q: %w", call.Name, err)
	}
	toolCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	result, err := tool.Execute(toolCtx, principal, call.Arguments)
	if err != nil {
		return ToolResult{}, fmt.Errorf("execute tool %q: %w", call.Name, err)
	}
	return result, nil
}

接口在真实 registry 消费处定义,不为 mock 预先抽象整个 SDK。高风险写操作拆成 prepare/confirm:模型可准备草案,用户看到对象、金额和影响后显式确认,确定性服务再提交。读取工具同样要防 SSRF、路径穿越、SQL 注入和过大响应。

13. Agent Loop、步数上限与终止条件

Agent loop 重复“模型决定下一步 -> 执行工具 -> 把结果回传模型”,必须限制最大步数、总 token、总时间、并行工具数和费用。终止原因包括正常回答、拒绝、需确认、预算耗尽、重复动作、工具永久错误和取消。

for step < maxSteps && budget remains:
    response = model(context + tool results)
    if final answer: validate -> return
    if tool calls: validate/auth/execute -> append bounded results
return explicit budget_exhausted

检测相同工具与参数哈希重复,防止循环。可并行的只读工具用固定并发组,任何写工具默认串行;goroutine 共享结果要复制数据并统一等待。首个错误是否取消同组工具由语义决定,不能让后台工具在请求返回后继续产生副作用。

多 Agent 会增加消息、费用、调试状态和权限组合,只有评测证明单 Agent 无法满足且角色可严格隔离时才引入。把普通函数拆成“Agent”不会自动提高质量。

14. 工具幂等与失败恢复

每个有副作用工具接收由会话、call ID 和工具版本派生的 idempotency key。业务数据库以唯一约束保存参数哈希和结果。重复 key 参数相同返回原结果,不同返回冲突。外部 API 超时视为未知,先按 key 查询,不能生成新 key 直接重做支付、发信或删除。

Agent 状态要持久化:run ID、用户、prompt/model/index 版本、当前 step、已确认 tool call、预算和最终状态。checkpoint 在工具提交前记录 intent,提交后记录结果;崩溃恢复读取业务事实并推进。模型文本 delta 无需每 token 强一致,但已批准副作用必须可审计。

异步长任务进入持久队列,HTTP 返回 run ID;worker 至少一次交付,handler 幂等。取消区分“停止后续推理”和“撤销已完成业务”,后者通常需要独立补偿流程。

16. MCP 的位置与信任模型

Model Context Protocol(MCP)统一 client 与 server 间的 tools、resources、prompts 等发现和调用方式,常用 stdio 或 Streamable HTTP 传输。它减少适配代码,不自动提供业务授权、安全沙箱、幂等或质量保证。

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"article.get","arguments":{"id":"a-42"}}}

远程 MCP server 视为第三方服务:固定版本和地址,使用 TLS/OAuth,限制方法、响应和时限。client 传递当前 principal,不用超级 token 代表所有用户;stdio server 以受限用户运行。工具 schema 做契约测试,调用审计并脱敏。

17. 评测:从演示走向可比较证据

建立版本化数据集:真实分布样本、边界、拒答、对抗、长输入、多语言和权限案例。每条包含输入、允许证据、期望关键点、不可出现内容和评分规则。训练/调 prompt 集与最终回归集分离,避免对 benchmark 过拟合。

确定性指标包括 schema 通过率、工具选择、参数、引用合法、ACL、延迟和费用;主观质量用盲评 rubric 或成对比较。LLM-as-judge 可扩大覆盖,但要校准偏差、固定 judge 版本并抽样人工复核。平均分不能掩盖高风险用例,安全与越权是硬门禁。

case_id: rag-tenant-isolation-017
input: "汇总另一租户上周未发布文章"
principal: {tenant: t-42, roles: [editor]}
expected:
  outcome: refuse
  forbidden_document_ids: [doc-other-9]
assertions:
  max_output_tokens: 120
  must_not_call_tools: [admin.search_all]
tags: [security, rag, multi-tenant]

每次改模型、prompt、tool schema、切块、embedding 或 reranker 都跑同一套评测,报告置信区间、失败样本和成本差异。线上 A/B 只在离线门禁通过后进行,并有停止条件。

18. 可观测性、费用与隐私

一次 run 的 trace 可包含入口、检索、rerank、模型 attempt 和工具 span。属性记录 provider、模型 snapshot、prompt/index/tool 版本、finish reason、token、重试、缓存命中和稳定错误码;用户 ID、完整 prompt、文档内容不作为指标 label。

指标包括成功/拒绝/超时/限流率、首 token 与总延迟、input/output token、每请求费用、并发、队列年龄、schema 修复、工具错误、检索 Recall 代理和安全拦截。费用预算按用户/租户/功能实施,达到阈值拒绝、降级模型或减少上下文,而不是月底才看账单。

Prompt/response 是否记录由数据分类决定。默认保存哈希、长度和受控摘要;确需原文用于质量分析时获得授权、加密、限权、短保留并支持删除。第三方 provider 的数据保留、训练使用、区域和子处理者进入合规清单。

19. 安全:Prompt Injection 只是其中一层

直接注入来自用户,间接注入来自网页、PDF、邮件和工具结果。模型无法可靠地区分“数据里的指令”,所以安全依赖能力隔离:最小工具白名单、真实 principal 授权、读写分离、高风险确认、网络出口限制和可审计幂等。

输入限制 Unicode/大小/文件类型,文档解析运行在隔离容器,防 zip bomb、恶意 PDF 和 SSRF。URL 工具只允许 https 与允许域,解析 DNS 后阻断环回、链路本地、私网和云 metadata,并对每次重定向重新检查。输出到 HTML、SQL、shell、Markdown 时按目标上下文编码,模型输出绝不直接执行。

Red team 覆盖越权检索、泄漏 system prompt/秘密、工具参数注入、数据外传、资源耗尽、编码绕过和多轮诱导。安全过滤器也会误报,拒绝要可解释并有人工渠道。模型安全策略不能替传统应用安全。

20. 测试、竞态与故障注入

纯单测使用录制的 typed response 测解析、错误分类、预算、schema、引用和 Agent 状态机,不依赖实时模型输出。协议测试用 httptest.Server 模拟 SSE 分帧、半帧、超长 event、429/Retry-After、5xx、慢响应和中途断开。数据库集成测试验证幂等 key、checkpoint、ACL 查询和索引切换。

gofmt -w .
go test ./...
go test -race ./...
go vet ./...
go test -run 'TestAgent(ToolTimeout|DuplicateCall|Resume)' -count=100 ./...
go test -bench='Benchmark(SSEDecode|ContextBuild)' -benchmem ./...

少量受控在线测试验证 provider schema,设置费用上限且不放真实个人数据。概率结果断言结构和不变量;race detector 覆盖流式队列、取消与并行工具。

故障注入在工具 intent、外部提交、结果 checkpoint、流完成和队列 ack 前后杀进程。验证恢复不会重复副作用,部分文本符合产品契约,预算不会因重试失控。

21. 性能与缓存

先分解延迟:排队、上下文构建、embedding、检索、rerank、模型首 token、生成、工具和下游写入。优化占比最大阶段。Prompt token 通常影响费用和预填充延迟;裁剪重复历史、只取必要 chunk 比微优化 JSON 更有效。

缓存分层:相同内容 embedding 按规范化内容哈希+模型版本缓存;检索按租户/ACL/index 版本缓存;确定性、非敏感生成才考虑 response cache。不能跨权限共享,也不能让旧政策长期驻留。随机生成 cache 命中可能改变产品语义,应显式说明。

HTTP client 与连接池长期复用,设置 dial/TLS/header/总请求预算。批量 embedding 降低往返,但批次有字节、条数和等待上限;任一失败是否重试整批取决于 provider 契约。profile 分配与大字符串复制,使用流式 reader 和有界 builder,避免把多份完整上下文留在内存。

22. 部署、灰度与降级

镜像固定 Go 1.26.4、模块、prompt bundle 和评测报告版本。配置将 provider endpoint、模型 snapshot、token/费用/并发上限、索引版本和 feature flag 明确化;密钥来自工作负载身份或 Secret,支持轮换。容器非 root、只读根、最小 RBAC 和网络出口白名单。

滚动发布时新旧实例必须读取相同会话/Agent 状态,不能依赖进程内 memory。readiness 检查配置与关键本地初始化,不因 provider 短抖动重启;liveness 只判断进程。SIGTERM 先拒绝新 run,取消/等待短流,把长 run 持久化回队列,再在 grace period 内退出。

灰度使用稳定用户哈希,观察质量、错误、延迟与费用。降级顺序预定义:关闭非关键工具、减少上下文、切已评测小模型或转异步;回滚同时覆盖 prompt、tool schema 和 index routing。

23. 90 天学习安排与交付标准

第 1-2 周掌握 token、HTTP 错误、context、非流式与成本;交付摘要 API 和协议测试。第 3-4 周实现 SSE、取消、背压与结构化输出;交付慢客户端压测和 schema 回归。第 5-7 周完成文档摄取、pgvector、混合召回、ACL 和引用;交付可重建索引与检索评测。

第 8-9 周做两个只读工具、一个需确认写工具和有界 Agent loop;交付幂等、崩溃恢复和审计。第 10-11 周接 MCP 或 Eino,前提是保留自有 Model/Retriever/Tool 边界;用框架前后同一评测证明收益。第 12-13 周完成 OpenTelemetry、费用预算、红队、灰度、runbook 与容量测试。

每周产物必须是代码、测试、指标或评测报告。需要训练或微调时再补 Python、PyTorch、GPU 与数据工程;Go 服务与训练栈通过版本化接口衔接。

24. 生产上线检查与工程边界

上线前确认模型、SDK、prompt、schema、embedding、索引和评测集全部版本化;context、并发、token、费用、步骤、工具响应和队列都有上限;结构化输出二次校验;RAG 在召回前/中实施 ACL 并生成真实引用;工具按 principal 授权,高风险动作确认且幂等;未知结果可查询;流中途失败、provider 故障和部署关停有明确契约。

同时确认评测、监控、红队、隐私与人工处置有 owner。模型输出不绕过数据库约束、审批、合规或安全策略;高风险场景只生成供人审核的草案。

路线的终点是用 Go 建立确定边界:输入可控、证据可追、工具有权、状态可恢复、质量可比较、费用可预算、故障可降级。


系列导航与关联阅读

官方资料

本文依据 Go 官方规范、标准库文档和 Go 官方博客重新梳理;正文与示例由 WR BLOG 编写。