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

Go AI 生产治理:提示注入、权限、限额、缓存、成本与降级

本文以 Go 1.26.4 为基准。AI 安全与成本不是两个独立附录:提示注入可能诱导工具外传数据并制造调用循环,无界上下文既泄漏隐私又推高 token,错误重试既放大上游故障又重复计费。生产治理要在模型外建立确定性的身份、数据、工具、网络和预算边界,让每个请求可授权、可限额、可审计、可取消和可降级。

1. 威胁模型与资产清单

先列资产:system prompt、用户输入、租户文档、访问令牌、工具结果、会话记忆、向量索引、缓存、模型输出、审计记录和预算。攻击者可能是匿名用户、已登录低权限用户、被污染网页或文件、恶意工具服务、供应商内部人员,也可能只是错误配置和失控重试。

画出信任边界:浏览器到 API、API 到检索库、编排器到工具、服务到模型供应商、日志到观测平台。每次跨界都标记 principal、用途、数据分类、允许动作、大小、时间和费用上限。模型既不可信任输入,也不是可信任决策者。

2. 提示注入无法靠提示词根治

直接注入来自用户,例如要求忽略系统规则;间接注入来自网页、PDF、邮件、工单、OCR 文本和工具返回。分隔符、角色说明和“永远不要”能改善模型行为,却不能形成安全边界,因为模型仍在同一上下文中解释数据与指令。

控制策略是缩小能力:只提供任务必需资料,来源标记为不可信数据;模型提出工具调用后由代码做 schema 校验、授权和确认;输出不能直接进入 shell、SQL 或模板。检索到“把秘密发送到某网址”时,即使模型遵循了它,网络出口和工具白名单也应使动作失败。

3. 输入分层与上下文构造

上下文按系统策略、开发配置、用户任务、已授权证据和工具结果分层。不要把不可信内容拼进 system instruction,也不要把用户可控字段用于模型名、base URL 或工具注册。对每层限制条数、字符与 token,超限时拒绝或按明确策略裁剪。

type ContextPart struct {
	Kind      string `json:"kind"`
	SourceID  string `json:"source_id"`
	Trust     string `json:"trust"`
	Content   string `json:"content"`
	MaxTokens int    `json:"max_tokens"`
}

func validatePart(part ContextPart) error {
	if part.Trust != "trusted-policy" && part.Trust != "untrusted-data" {
		return fmt.Errorf("unknown trust level %q", part.Trust)
	}
	if part.MaxTokens <= 0 || len(part.Content) > 256<<10 {
		return errors.New("context part exceeds limits")
	}
	return nil
}

token 估算只是 admission control,最终以供应商 usage 对账。Unicode 混淆、Base64 和多语言会绕过关键词过滤,因此过滤器只能提供信号,不能替代权限隔离。

4. 数据泄漏的主要路径

泄漏可能发生在回答、引用、工具参数、错误、日志、trace、缓存键、训练反馈和 provider retention。RAG 必须在 chunk 进入模型前按真实 principal、tenant、ACL 版本过滤;不能先跨租户召回再让模型不要回答。引用 ID 由系统生成并验证,模型不能自由编 URL。

system prompt 通常不是密码,但其中可能暴露内部控制和连接信息;真正秘密绝不进入 prompt。日志记录版本哈希、长度、类别和 request ID,不记录完整 Authorization、签名 URL、个人数据或工具原始响应。排障原文单独受控、加密并短期留存。

5. 工具调用遵循最小权限

模型返回的工具名和 JSON 参数只是提议。服务端从固定 registry 查找,严格解码未知字段,绑定当前 principal 再授权。读取和写入工具使用不同能力;高风险写操作采用 prepare/confirm,确认页面展示真实对象、金额与影响,最终提交由业务服务执行。

type ToolCall struct {
	ID        string          `json:"id"`
	Name      string          `json:"name"`
	Arguments json.RawMessage `json:"arguments"`
}

func executeTool(ctx context.Context, principal Principal, call ToolCall, tools map[string]Tool) (Result, error) {
	tool, ok := tools[call.Name]
	if !ok {
		return Result{}, fmt.Errorf("tool %q is not allowed", call.Name)
	}
	if err := authorize(principal, tool.Action()); err != nil {
		return Result{}, 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 Result{}, fmt.Errorf("execute tool %q: %w", call.Name, err)
	}
	return result, nil
}

接口定义在 registry 的使用侧,不为 mock 抽象整个供应商 SDK。工具结果也视为不可信内容,裁剪大小、标注来源、剥离秘密后才能回传模型。

6. 工具幂等、未知结果与确认

副作用工具接收由 tenant、run、call ID 和工具版本派生的 idempotency key。业务库以唯一约束保存参数哈希、状态和结果;相同 key、相同参数返回旧结果,不同参数返回冲突。外部 API 超时表示结果未知,先查询状态,不能换 key 重做支付或发送。

确认 token 绑定 principal、资源、参数哈希、过期时间和单次 nonce,不能只表示“用户点过确认”。模型在确认后修改金额或收件人必须重新确认。撤销已完成动作是独立补偿流程,取消 Agent 不能假装回滚真实世界。

7. 网络与执行沙箱

URL 工具只允许 HTTPS、批准域名和端口。解析 DNS 后拒绝环回、私网、链路本地、组播和云 metadata;每次重定向重新检查,限制跳数、响应大小和时限。DNS rebinding 防护要求连接到已验证地址并正确验证 TLS 主机名。

代码执行、浏览器和文件解析在无生产凭据的隔离运行时中执行,使用非 root、只读根、临时工作区、CPU/内存/进程/网络限制。命令和参数由服务端模板生成,不把模型文本交给 shell。MCP 或第三方工具协议统一了调用格式,不自动提供这些安全保证。

8. 分层预算模型

预算至少有单请求、单 run、用户/租户日额度、服务月额度和供应商账户硬上限。资源包括输入 token、输出 token、embedding、图片、音频秒、工具次数、Agent 步数、并发和墙钟时间。请求进入前预留上界,结束后按实际 usage 结算并释放差额。

{
  "tenant_id": "tenant_42",
  "period": "2026-08-31",
  "limits": {
    "requests": 20000,
    "input_tokens": 8000000,
    "output_tokens": 1200000,
    "audio_seconds": 36000,
    "tool_calls": 50000,
    "cost_usd_micros": 250000000
  },
  "hard_stop_at_percent": 100,
  "alert_at_percent": [70, 90]
}

金额用最小单位整数或十进制定点,不用 float64 累计账单。价格表按供应商、区域、模型 snapshot、生效时间和单位版本化,历史账单不应用新价格回算。

9. 原子预留避免并发超卖

只在请求结束后累计会让大量并发同时穿透预算。admission 阶段估算最坏成本,原子执行“检查余额并预留”;完成后结算,超时任务最终也要对账。Redis Lua 或数据库条件更新可保证单存储边界内原子性,但跨区强一致成本需明确。

type Reservation struct {
	ID             string
	TenantID       string
	ReservedMicros int64
	ExpiresAt      time.Time
}

func reserve(ctx context.Context, store Store, tenantID string, micros int64) (Reservation, error) {
	if micros <= 0 {
		return Reservation{}, errors.New("reservation must be positive")
	}
	reservation, err := store.Reserve(ctx, tenantID, micros)
	if err != nil {
		return Reservation{}, fmt.Errorf("reserve AI budget: %w", err)
	}
	return reservation, nil
}

预留需有过期回收,但不能让仍运行的任务因租约过期获得免费额度;worker 定期续租或以持久状态对账。幂等请求重复提交复用原 reservation 和结果。

10. 速率、并发和队列是三种限制

令牌桶控制一段时间内到达速率,semaphore 控制在途昂贵操作,队列限制等待工作。只做 QPS 限制无法阻止长生成占满连接;只限并发又允许突发快速请求耗尽日预算。限制按租户和全局双层执行,防止一个大租户挤压所有用户。

队列等待计入总 deadline。队列满或预计等待超预算时立即返回明确的 429/503 和重试建议,不启动后台“幽灵请求”。重试遵循 Retry-After、指数退避、full jitter、总次数和总费用;认证、schema、上下文过长等确定错误不重试。

11. Agent 循环设置硬终止条件

Agent loop 同时限制最大 step、总 token、工具次数、同参数重复次数、并行工具数、时间和费用。相同工具名与规范化参数哈希连续出现时终止为 repeated_action。写工具默认串行,只读工具并行也受固定组限制。

agent_limits:
  max_steps: 8
  max_model_calls: 9
  max_tool_calls: 12
  max_parallel_read_tools: 3
  max_same_call_repeats: 1
  max_input_tokens: 24000
  max_output_tokens: 4000
  max_duration: 45s
  max_cost_usd_micros: 35000

预算耗尽是正常终止原因,不应包装成 500。返回用户可理解的部分结果或“需要缩小任务”,并确保在途 goroutine、reader 和工具调用退出。

12. 缓存首先解决权限正确性

embedding 缓存键包含规范化内容哈希和 embedding 模型版本;检索缓存再包含 tenant、principal 权限版本、索引版本、query 和过滤配置;生成缓存包含模型 snapshot、prompt/schema 版本、完整授权上下文哈希和生成参数。缺少任何维度都可能串租户或返回旧政策。

随机生成不一定适合 response cache。对可缓存的确定性问答定义 TTL、失效事件和“命中是否计费”的产品语义。缓存值按数据分类加密,key 不含原始 PII。删除用户数据时同时清理派生缓存;仅等 TTL 可能不满足删除 SLA。

13. 模型路由要先通过评测

路由器根据任务、风险、上下文长度、区域、健康、延迟和剩余预算选择已批准模型。低风险摘要可使用已评测的小模型,高风险判断可能必须人工处理;不能因为预算不足静默切到未知质量模型。路由决策保存规则版本和原因。

{
  "routing_policy": "ai-router-v12",
  "rules": [
    {"when":{"risk":"high"},"route":"review-required"},
    {"when":{"task":"classification","max_input_tokens":4000},"route":"small-model-2026-08"},
    {"when":{"region":"cn","data_class":"restricted"},"route":"approved-local-model"}
  ],
  "fallbacks": {"small-model-2026-08":["async-queue"],"approved-local-model":["unavailable"]}
}

健康切换使用熔断和探测,避免每请求在多个供应商间串行试错。路由前预估成本,路由后使用真实 usage 对账。提供商返回的模型解析 ID必须进入审计。

14. 降级不是绕过策略

预定义降级顺序:减少非必要历史和检索候选、降低最大输出、关闭非关键工具、转异步、切换已评测模型、返回传统搜索或明确不可用。安全过滤、ACL、确认和审计永不因高负载关闭。若所有合规区域模型不可用,正确结果可能就是拒绝服务。

降级状态通过稳定错误码和响应字段告知客户端,并记录触发原因、持续时间与质量影响。恢复采用滞回与小流量探测,防止在供应商边缘状态反复切换。

15. 审计事件要可回答责任问题

审计应回答谁以什么身份、基于哪个策略、访问了什么类别数据、选择哪个模型、提议和执行了什么工具、预算如何变化、结果为何被允许或拒绝。记录稳定 ID和参数哈希,敏感 payload 放受控证据库,不能用普通应用日志代替审计账本。

{
  "event_id":"evt_01K4","occurred_at":"2026-08-31T10:15:30Z",
  "tenant_id":"tenant_42","principal_id":"user_7","run_id":"run_88",
  "action":"tool.execute","resource":"invoice:inv_19","decision":"denied",
  "policy_version":"authz-v31","reason_code":"confirmation_required",
  "model_snapshot":"model-2026-08-20","arguments_sha256":"a184...",
  "trace_id":"4bf92f3577b34da6"
}

事件使用 UTC RFC3339 时间、不可变 event ID 和 schema 版本。访问审计本身也要审计;设保留期、完整性保护和导出审批,避免它成为集中泄漏源。

16. 观测成本而不暴露内容

指标按 provider、模型族、任务、状态和租户层级的受控低基数维度聚合,记录请求、token、媒体单位、工具数、预留与实际差额、缓存命中、路由和重试。不要把 user ID、prompt 或完整模型名动态值直接作 metric label。

trace 展示排队、检索、模型、工具与结算阶段,错误事件记录分类而非原始响应。日报用 provider 账单对账内部 usage;差异超过阈值检查重试、流中断无 usage、价格版本和时区。告警同时看费用速率和绝对预算,避免月末才发现异常。

17. 错误分类与事故诊断

安全错误包括认证失败、ACL 拒绝、注入信号、数据分类不允许、工具越权和确认缺失;容量错误包括速率、并发、队列和预算;依赖错误包括 429、5xx、超时和协议变化。用户错误不重试,上游暂时错误在预算内重试,工具未知结果先查状态。

费用突增按顺序查:流量是否真实增长,单请求输入/输出是否变化,缓存命中是否下降,路由是否切到贵模型,重试与 Agent 步数是否增加,价格表是否变更。泄漏事件立即撤销凭据、停止相关 route、保存最小证据、定位受影响 principal 与对象,并执行通知和删除流程。

18. 安全与成本测试

单测覆盖策略矩阵、预算边界、金额溢出、缓存键隔离、工具 schema、重复 idempotency key 和状态转换。表驱动用例保持同类行为,复杂授权路径拆成独立测试。fuzz JSON 参数、URL、Unicode 和流解析器,断言不 panic 且输入始终有界。

func TestValidatePart(t *testing.T) {
	tests := []struct {
		name    string
		give    ContextPart
		wantErr bool
	}{
		{name: "trusted", give: ContextPart{Trust: "trusted-policy", Content: "policy", MaxTokens: 10}},
		{name: "unknown trust", give: ContextPart{Trust: "external", Content: "x", MaxTokens: 10}, wantErr: true},
		{name: "missing budget", give: ContextPart{Trust: "untrusted-data", Content: "x"}, wantErr: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := validatePart(tt.give); (err != nil) != tt.wantErr {
				t.Errorf("validatePart() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

集成测试模拟跨租户 ID、恶意检索文档、私网 URL、工具超时、Redis/数据库冲突、provider 429 和 usage 缺失。go test -race ./... 覆盖并发预留与缓存;故障注入验证崩溃后不会重复副作用或释放错误额度。

19. 性能与容量边界

治理路径本身不能成为无界瓶颈。测授权查询、预算预留、缓存、路由和审计写入的 P95/P99 及失败模式。授权与预算宁可明确拒绝也不能在依赖失败时默认放行。审计采用持久队列时定义最大积压和 fail-closed 的高风险动作。

连接池长期复用,deadline 从入口向下递减。减少 token 往往比微优化 JSON 更有效,但上下文裁剪必须通过质量与安全评测。批处理 embedding 或 Judge 能降往返成本,批次仍需字节、等待时间和独立 case 错误边界。

20. 密钥、供应链和部署

密钥来自工作负载身份或 Secret Manager,按服务、环境和 provider 隔离,支持轮换、吊销和使用审计。启动日志只记录 secret 引用名。依赖与镜像固定版本、生成 SBOM、扫描已知漏洞;模型、prompt、路由、价格和策略同样是发布制品。

容器非 root、只读根、最小 RBAC 和网络出口白名单。readiness 验证本地配置与必要控制面,不因 provider 抖动不断重启;liveness 只检查进程。SIGTERM 停止接收新 run,取消在途只读调用,checkpoint 长任务,并等待 goroutine 退出。

21. 灰度、门禁和应急开关

候选模型、prompt、工具或路由规则先通过固定数据集:跨租户泄漏和未确认写入必须为零,质量、延迟和单位成本满足阈值。灰度按稳定 principal 哈希,观察错误、拒绝、工具、预算和投诉。回滚要同时恢复相互依赖的 prompt、schema、模型和路由。

应急开关按能力粒度设计:暂停某工具、租户、模型或数据源,而不是关闭全部审计。开关变更需要权限、原因、过期时间和审计,默认自动恢复前必须重新评估条件。

22. 生产上线检查

上线前逐项确认:所有输入与检索数据在进模型前授权;直接和间接注入不能扩大能力;工具白名单、严格 schema、principal 授权、确认与幂等齐备;URL 和代码执行隔离;请求、Agent、租户与账户都有硬预算;缓存键包含租户、ACL 和版本;路由与降级仅使用已评测方案;日志、trace 和审计不泄密;删除覆盖记忆、缓存和 provider 文件。

同时演练供应商全挂、预算存储不可用、凭据泄漏、费用突增、跨租户命中和工具未知结果。AI 系统不能承诺模型永不受注入影响,但可以保证即使模型作出恶意建议,确定性控制仍阻止越权、外传、无限消费和不可逆副作用。这就是 Go 服务在 AI 生产架构中的核心责任。


系列导航与关联阅读

官方资料

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