第九章 平台级 AI 支撑架构
AI 能力进入产品矩阵后,很容易从“一个接口调用”变成“不可控的成本和风险来源”。平台级 AI 架构要解决的不只是调用模型,还包括模板版本、知识检索、异步任务、租户配额、内容安全、成本计量和人工复核。
一个成熟 AI 支撑层应像支付中心一样被治理:谁调用、调用了什么、花了多少钱、生成了什么、是否可追溯,都要清楚。
9.1 AI 微服务封装
AI 服务建议独立为平台服务,业务模块通过统一接口调用。
graph TD
A[Business Service] --> B[AI Gateway]
B --> C[Prompt Service]
B --> D[Model Router]
B --> E[Safety Guard]
B --> F[Usage Meter]
D --> G[OpenAI / Claude / Local Model]
B --> H[Task Queue]
统一封装的好处:
| 能力 | 价值 |
|---|---|
| 模型切换 | 不影响业务模块 |
| 成本统计 | 可按租户计费 |
| 安全策略 | 输入输出统一治理 |
| 失败重试 | 长任务可恢复 |
| 缓存 | 避免重复生成 |
9.2 Prompt 模板系统
Prompt 不应该散落在业务代码里。它应像配置和代码一样有版本。
模板字段:
| 字段 | 说明 |
|---|---|
template_id | 模板标识 |
version | 版本号 |
variables | 允许传入的变量 |
model_policy | 推荐模型和参数 |
safety_level | 安全等级 |
owner | 负责人 |
一次 Prompt 变更要能灰度。比如只对某个租户启用新版客服回复模板,观察满意度和人工接管率后再扩大。
9.3 模型适配层
模型适配层要屏蔽不同供应商 API 差异。
统一接口可以表达为:
type ModelClient interface {
Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
Embed(ctx context.Context, req EmbedRequest) (*EmbedResponse, error)
}
模型选择策略:
| 任务 | 推荐策略 |
|---|---|
| 客服问答 | 低延迟、成本可控 |
| 合同分析 | 长上下文、准确性优先 |
| 内容生成 | 创造性和安全平衡 |
| 向量检索 | 统一 embedding 模型 |
| 批量摘要 | 异步、低成本模型 |
不要让业务代码直接写供应商名称。应通过任务类型、租户配置和成本策略来路由。
9.4 向量检索服务
向量检索适合解决“语义相关”的问题,但不是万能数据库。
典型链路:
文档上传 -> 切分 chunk -> 生成 embedding -> 写入向量库
用户问题 -> 生成 query embedding -> 检索 TopK -> 重排序 -> 拼接上下文 -> 调用模型
切分策略很重要:
| 内容 | 建议 |
|---|---|
| FAQ | 一问一答为 chunk |
| 长文档 | 按标题和段落切分 |
| 代码文档 | 按函数、类、章节切分 |
| 合同 | 按条款切分 |
向量库可以选择 PgVector、Milvus、Qdrant 等。早期如果数据量不大,PgVector 更容易运维。
9.5 RAG 与数据增强
RAG 的关键不是“检索 + 生成”,而是让模型知道哪些内容可信、哪些内容只是参考。
上下文建议包含:
- 文档片段;
- 来源链接;
- 更新时间;
- 租户和权限信息;
- 置信度。
回答输出应保留引用信息,尤其是知识库问答、内部政策、财务说明等高风险场景。
9.6 AIGC 输出管理
平台要防止三类问题:
| 问题 | 表现 | 防护 |
|---|---|---|
| Prompt 注入 | 用户要求模型忽略系统规则 | 输入过滤、上下文隔离 |
| 滥用生成 | 批量生成垃圾内容 | 配额、频率限制、审核 |
| 重复生成 | 同一请求反复消耗成本 | 幂等 key、结果缓存 |
高风险任务应进入人工复核,例如医疗建议、金融建议、法律文本、公开发布内容。
9.7 与产品矩阵融合
AI 不应只是一个“生成按钮”。它可以嵌入不同业务域:
| 产品域 | AI 能力 |
|---|---|
| 内容域 | 标题建议、摘要、标签、审核 |
| 电商域 | 商品描述、客服回复、评价总结 |
| 工具域 | OCR、表格理解、文档问答 |
| 分析域 | 数据洞察、异常解释 |
| 推广域 | 广告文案、受众分群 |
| 客服域 | 知识库问答、工单摘要 |
每个融合点都要记录输入、输出、模型、模板版本和人工修改结果。这些数据会反过来优化 Prompt 和产品体验。
9.8 AI 成本与计费
AI 成本必须可见,否则很容易被几个租户打穿预算。
记录维度:
| 维度 | 示例 |
|---|---|
| 租户 | 哪个客户产生费用 |
| App | 哪个产品使用 |
| 功能 | 摘要、问答、生成 |
| 模型 | 使用了哪个模型 |
| Token | 输入、输出、总量 |
| 结果 | 成功、失败、重试 |
套餐可以按“次数 + token + 高级模型额度”组合计费。
9.9 AI 支撑架构清单
| 检查项 | 标准 |
|---|---|
| 调用入口 | 业务不直连模型供应商 |
| Prompt | 模板版本化、可灰度、可回滚 |
| 检索 | 向量库按租户隔离 |
| 安全 | 输入输出有治理策略 |
| 成本 | token 和调用量可按租户统计 |
| 审计 | 关键输出可追溯 |
| 产品融合 | AI 能力沉淀为平台服务 |
9.10 代码实践:AI Gateway、模型适配与 RAG 检索
一、AI Gateway 统一入口
package aigateway
import (
"context"
"fmt"
"time"
)
// AIRequest 统一请求结构
type AIRequest struct {
TenantID string `json:"tenant_id"`
AppID string `json:"app_id"`
TaskType string `json:"task_type"` // chat / embed / summarize
PromptTemplate string `json:"prompt_template"`
Variables map[string]interface{} `json:"variables"`
Temperature float64 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
Timeout time.Duration `json:"timeout"`
SafetyLevel string `json:"safety_level"` // low / medium / high
}
type AIResponse struct {
Content string `json:"content"`
ModelUsed string `json:"model_used"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
CostUSD float64 `json:"cost_usd"`
Cached bool `json:"cached"`
}
type Gateway struct {
router ModelRouter
meter UsageMeter
safety SafetyGuard
cache Cache
templates PromptTemplateStore
}
func (g *Gateway) Call(ctx context.Context, req AIRequest) (*AIResponse, error) {
// 1. 检查租户配额
if err := g.meter.CheckQuota(ctx, req.TenantID, req.TaskType); err != nil {
return nil, fmt.Errorf("quota exceeded: %w", err)
}
// 2. 加载 Prompt 模板并填充变量
prompt, err := g.templates.Render(ctx, req.PromptTemplate, req.Variables)
if err != nil {
return nil, err
}
// 3. 安全审查(输入过滤)
if req.SafetyLevel == "high" {
if blocked := g.safety.CheckInput(ctx, prompt); blocked {
return nil, fmt.Errorf("input blocked by safety policy")
}
}
// 4. 缓存命中检查
cacheKey := Hash(prompt + req.TaskType)
if cached := g.cache.Get(ctx, cacheKey); cached != nil {
return cached.(*AIResponse), nil
}
// 5. 模型路由
client, modelConfig := g.router.Select(ctx, req.TaskType, req.TenantID)
// 6. 调用模型(带超时)
callCtx, cancel := context.WithTimeout(ctx, req.Timeout)
defer cancel()
resp, err := client.Chat(callCtx, ChatRequest{
Messages: []Message{{Role: "user", Content: prompt}},
Temperature: req.Temperature,
MaxTokens: req.MaxTokens,
})
if err != nil {
return nil, fmt.Errorf("model call failed: %w", err)
}
// 7. 安全审查(输出过滤)
if req.SafetyLevel != "low" {
if flagged := g.safety.CheckOutput(ctx, resp.Content); flagged {
return nil, fmt.Errorf("output flagged by safety policy")
}
}
result := &AIResponse{
Content: resp.Content,
ModelUsed: modelConfig.Name,
InputTokens: resp.Usage.PromptTokens,
OutputTokens: resp.Usage.CompletionTokens,
CostUSD: modelConfig.CalcCost(resp.Usage.PromptTokens, resp.Usage.CompletionTokens),
Cached: false,
}
// 8. 记录用量
g.meter.Record(ctx, req.TenantID, req.AppID, req.TaskType, result)
// 9. 缓存结果
g.cache.Set(ctx, cacheKey, result, 5*time.Minute)
return result, nil
}
二、模型适配器(多供应商统一接口)
package modelclient
import (
"context"
"fmt"
)
// ModelClient 统一模型调用接口
type ModelClient interface {
Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
Embed(ctx context.Context, req EmbedRequest) (*EmbedResponse, error)
Name() string // 供应商标识
}
// OpenAIAdapter OpenAI / Azure 适配器
type OpenAIAdapter struct {
client *openai.Client
model string
}
func (a *OpenAIAdapter) Name() string { return "openai:" + a.model }
func (a *OpenAIAdapter) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
msgs := make([]openai.ChatCompletionMessageParamUnion, len(req.Messages))
for i, m := range req.Messages {
msgs[i] = openai.UserMessage(m.Content)
}
resp, err := a.client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Model: openai.F(a.model),
Messages: openai.F(msgs),
})
if err != nil {
return nil, err
}
return &ChatResponse{
Content: resp.Choices[0].Message.Content,
Usage: Usage{PromptTokens: int(resp.Usage.PromptTokens), CompletionTokens: int(resp.Usage.CompletionTokens)},
}, nil
}
// LocalAdapter 本地 Ollama / vLLM 适配器
type LocalAdapter struct {
baseURL string
model string
}
func (a *LocalAdapter) Name() string { return "local:" + a.model }
func (a *LocalAdapter) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
// 调用本地 HTTP 接口
// 实现略,结构与 OpenAIAdapter 类似
return &ChatResponse{}, fmt.Errorf("not implemented")
}
// Router 按任务类型和成本策略选择模型
type Router struct {
clients map[string][]ModelClient // task_type -> clients
configs map[string]ModelConfig
}
type ModelConfig struct {
Name string
MaxTokens int
CostPer1KInput float64
CostPer1KOutput float64
LatencyMS int
}
func (r *Router) Select(ctx context.Context, taskType, tenantID string) (ModelClient, ModelConfig) {
candidates := r.clients[taskType]
// 简化策略:优先成本低,兜底稳定性
return candidates[0], r.configs[candidates[0].Name()]
}
func (c ModelConfig) CalcCost(inputTokens, outputTokens int) float64 {
return float64(inputTokens)*c.CostPer1KInput/1000 + float64(outputTokens)*c.CostPer1KOutput/1000
}
三、Prompt 模板版本管理
package prompt
import (
"bytes"
"context"
"encoding/json"
"fmt"
"html/template"
"time"
)
type Template struct {
ID string `json:"id"`
Version string `json:"version"`
Content string `json:"content"` // Go template syntax
Variables []string `json:"variables"`
ModelPolicy string `json:"model_policy"` // chat / gpt-4 / local-llama
SafetyLevel string `json:"safety_level"`
Owner string `json:"owner"`
CreatedAt time.Time `json:"created_at"`
}
func (t *Template) Render(variables map[string]interface{}) (string, error) {
tmpl, err := template.New("prompt").Parse(t.Content)
if err != nil {
return "", fmt.Errorf("invalid template: %w", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, variables); err != nil {
return "", fmt.Errorf("render failed: %w", err)
}
return buf.String(), nil
}
// TemplateStore 模板存储与版本管理
type TemplateStore struct {
db *gorm.DB
}
func (s *TemplateStore) Create(ctx context.Context, tmpl *Template) error {
// 新模板默认 v1
tmpl.Version = "v1"
tmpl.CreatedAt = time.Now()
return s.db.Create(tmpl).Error
}
func (s *TemplateStore) Update(ctx context.Context, tmpl *Template) error {
// 版本号自增
tmpl.Version = bumpVersion(tmpl.Version)
return s.db.Create(tmpl).Error // 保留历史版本
}
func (s *TemplateStore) GetLatest(ctx context.Context, templateID, tenantID string) (*Template, error) {
var tmpl Template
err := s.db.Where("id = ? AND (target_tenant IS NULL OR target_tenant = ?)", templateID, tenantID).
Order("created_at DESC").
First(&tmpl).Error
return &tmpl, err
}
四、RAG 检索与上下文组装
package rag
import (
"context"
"fmt"
"sort"
)
// Retriever 执行向量检索 + 重排序 + 上下文拼接
type Retriever struct {
embedder Embedder
vectorDB VectorStore
reRanker ReRanker
}
func (r *Retriever) Retrieve(ctx context.Context, query string, topK int) (*Context, error) {
// 1. 生成 query embedding
embedding, err := r.embedder.Embed(ctx, query)
if err != nil {
return nil, fmt.Errorf("embedding failed: %w", err)
}
// 2. 向量检索(粗排)
candidates, err := r.vectorDB.Search(ctx, embedding, topK*3)
if err != nil {
return nil, fmt.Errorf("vector search failed: %w", err)
}
// 3. 重排序(精排)
scored := r.reRanker.Rank(query, candidates)
sort.Slice(scored, func(i, j int) bool { return scored[i].Score > scored[j].Score })
if len(scored) > topK {
scored = scored[:topK]
}
// 4. 组装上下文
ctx := &Context{
Query: query,
References: make([]Reference, len(scored)),
TotalTokens: 0,
}
for i, s := range scored {
ctx.References[i] = Reference{
Document: s.Document,
Score: s.Score,
Source: s.Document.Meta.Source,
UpdatedAt: s.Document.Meta.UpdatedAt,
}
ctx.TotalTokens += s.Document.TokenCount
}
return ctx, nil
}
// BuildPrompt 将检索结果和系统指令拼接为最终 Prompt
func BuildPrompt(query string, ctx *Context, systemPrompt string) string {
var buf bytes.Buffer
buf.WriteString(systemPrompt)
buf.WriteString("\n\n## 参考资料(按相关性排序):\n")
for i, ref := range ctx.References {
buf.WriteString(fmt.Sprintf("[%d] %s(来源:%s,更新时间:%s)\n%s\n\n",
i+1, ref.Document.Title, ref.Source, ref.UpdatedAt.Format("2006-01-02"), ref.Document.Content))
}
buf.WriteString("## 用户问题:\n")
buf.WriteString(query)
buf.WriteString("\n\n请基于参考资料回答,如果参考资料不足以回答,请说明。")
return buf.String()
}
五、异步 AI 任务与人工复核流
package aitask
import (
"context"
"encoding/json"
"time"
)
// AITask 异步任务模型
type AITask struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
AppID string `json:"app_id"`
Type string `json:"type"` // summarize / generate / classify
Status string `json:"status"` // pending / running / completed / failed / review
Input json.RawMessage `json:"input"`
Output string `json:"output"`
ModelUsed string `json:"model_used"`
ReviewFlag string `json:"review_flag"` // auto / manual / rejected
ReviewerID string `json:"reviewer_id"`
CostUSD float64 `json:"cost_usd"`
CreatedAt time.Time `json:"created_at"`
CompletedAt *time.Time `json:"completed_at"`
}
// TaskWorker 消费异步 AI 任务队列
func (w *TaskWorker) Process(ctx context.Context, taskID string) error {
var task AITask
if err := w.db.First(&task, "id = ?", taskID).Error; err != nil {
return err
}
// 更新状态为 running
w.db.Model(&task).Update("status", "running")
var req aigateway.AIRequest
_ = json.Unmarshal(task.Input, &req)
resp, err := w.gateway.Call(ctx, req)
if err != nil {
w.db.Model(&task).Updates(map[string]interface{}{
"status": "failed",
"output": err.Error(),
})
return err
}
// 判断是否需要人工复核
reviewFlag := "auto"
if req.SafetyLevel == "high" || ContainsHighRiskKeywords(resp.Content) {
reviewFlag = "manual"
}
completedAt := time.Now()
w.db.Model(&task).Updates(map[string]interface{}{
"status": "completed",
"output": resp.Content,
"model_used": resp.ModelUsed,
"cost_usd": resp.CostUSD,
"review_flag": reviewFlag,
"completed_at": &completedAt,
})
// 触发通知
if reviewFlag == "manual" {
w.notify.ReviewPending(task.TenantID, task.ID)
}
return nil
}
本章小结
本章构建了平台级 AI 支撑架构的完整治理框架:统一 AI Gateway 封装模型调用、Prompt 模板版本化灰度、向量检索(RAG)与数据增强、AIGC 输出安全治理,以及按租户/功能/模型的多维成本计量体系。核心原则是:AI 能力要像支付中心一样被治理,谁调用、调用什么、花多少、生成什么,都必须可追溯。
延伸阅读
- 上一章:高可用与可靠性 — 限流熔断、容灾多活与性能基准测试。
- 下一章:DevOps 与系统可观测性 — CI/CD 流水线、可观测性三大支柱、K3s 与自动伸缩的完整方案。
关联专题
| 专题 | 关联内容 | 链接 |
|---|---|---|
| PostgreSQL | 向量数据库与PgVector | /posts/postgresql/ |
| TypeScript | AI接口类型安全 | /posts/typescript/ |
| Vercel | AI应用前端部署 | /posts/vercel/ |
| Node.js | 多后端AI运行时统一 | /posts/nodejs/ |
| Cloudflare | 边缘AI推理部署 | /posts/cloudflare/ |
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。