「产品矩阵平台」平台级 AI 支撑架构

平台级 AI 支撑架构:AI 微服务、Prompt 模板、模型适配、RAG 向量检索、多租户配额管理与输出治理的安全合规设计。

第九章 平台级 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 的关键不是“检索 + 生成”,而是让模型知道哪些内容可信、哪些内容只是参考。

上下文建议包含:

  1. 文档片段;
  2. 来源链接;
  3. 更新时间;
  4. 租户和权限信息;
  5. 置信度。

回答输出应保留引用信息,尤其是知识库问答、内部政策、财务说明等高风险场景。

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 能力要像支付中心一样被治理,谁调用、调用什么、花多少、生成什么,都必须可追溯。


延伸阅读


关联专题

专题关联内容链接
PostgreSQL向量数据库与PgVector/posts/postgresql/
TypeScriptAI接口类型安全/posts/typescript/
VercelAI应用前端部署/posts/vercel/
Node.js多后端AI运行时统一/posts/nodejs/
Cloudflare边缘AI推理部署/posts/cloudflare/

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「SaaS」更多文章

  1. 「产品矩阵平台」未来演进方向
  2. 「产品矩阵平台」运维与成本优化
  3. 「产品矩阵平台」安全与合规体系