RAG 部署与可观测性:日志、追踪、脱敏和回滚
开发完成只是第一步,如何将 RAG 服务稳定地运行在生产环境,并对其进行监控,是工程化的最后“一公里”。
1. Docker 容器化部署
Golang 的最大优势之一是编译产物极小(静态链接二进制)。我们可以使用 多阶段构建 (Multi-stage Build) 来制作超小的 Docker 镜像。
Dockerfile 示例
# 阶段 1: 编译
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 设置代理 (国内环境)
ENV GOPROXY=https://goproxy.cn,direct
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 编译为名为 server 的二进制文件
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/api
# 阶段 2: 运行 (使用 distroless 或 alpine)
FROM alpine:latest
WORKDIR /root/
# 安装 ca-certificates (调用 HTTPS API 需要)
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/server .
COPY --from=builder /app/config.yaml .
EXPOSE 8080
CMD ["./server"]构建并运行:
docker build -t go-rag-service .
docker run -p 8080:8080 -e OPENAI_API_KEY=sk-xxx go-rag-service这段 Dockerfile 用于说明多阶段构建,不是完整生产模板。实际发布应固定并扫描基础镜像、使用非 root 用户、通过密钥管理系统注入凭据、设置只读文件系统与资源限制,并在 CI 中生成可追溯的软件物料与镜像摘要。不要把真实 API Key 写入命令历史、镜像层、配置文件或仓库。
2. 可观测性 (Observability)
RAG 系统链路长(API -> Embedding -> VectorDB -> LLM),一旦变慢,很难排查是哪一步的问题。 我们需要引入 OpenTelemetry 进行链路追踪 (Tracing)。
Golang 接入 OpenTelemetry
package main
import (
"context"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)
// 初始化 Tracer
func initTracer(url string) func(context.Context) error {
exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
if err != nil {
log.Fatal(err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("go-rag-service"),
)),
)
otel.SetTracerProvider(tp)
return tp.Shutdown
}
// 在业务代码中打点
func (uc *ChatUseCase) Chat(ctx context.Context, query string) (string, error) {
tr := otel.Tracer("usecase")
ctx, span := tr.Start(ctx, "Chat")
defer span.End()
// 1. Embedding
_, embedSpan := tr.Start(ctx, "Embedding")
// ... 调用 Embedding API ...
embedSpan.End()
// 2. Retrieval
_, searchSpan := tr.Start(ctx, "VectorSearch")
// ... 调用 Milvus ...
searchSpan.SetAttributes(attribute.Int("top_k", 5))
searchSpan.End()
// 3. LLM Generation
_, llmSpan := tr.Start(ctx, "LLMGenerate")
// ... 调用 OpenAI ...
llmSpan.End()
return "result", nil
}3. 监控指标 (Metrics)
使用 Prometheus 监控关键指标:
- QPS: 每秒请求数。
- Latency: 接口响应时间 (P99, P95)。
- Token Usage: 消耗的 Token 数量(直接关联成本)。
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP请求耗时分布",
},
[]string{"path"},
)
)
func init() {
prometheus.MustRegister(requestDuration)
}
// 在 Handler 中记录
func Handler(c *gin.Context) {
timer := prometheus.NewTimer(requestDuration.WithLabelValues(c.FullPath()))
defer timer.ObserveDuration()
// ... 处理业务 ...
}指标标签不要直接使用用户 ID、原始 Query、完整 URL 或 Chunk 文本,以免产生高基数和敏感数据泄露。延迟分位数应由后端根据直方图聚合,告警阈值来自服务目标和实测基线,而不是照抄示例值。
4. RAG 日志与追踪应该记录什么
一次请求需要同时解释“系统为什么慢”和“答案为什么错”。建议让 request ID 贯穿网关、Embedding、检索、重排和生成,并记录:
| 阶段 | 建议记录 | 默认不记录 |
|---|---|---|
| 请求 | 路由、租户/用户的不可逆标识、超时与结果类别 | 原始身份信息、完整问题 |
| 检索 | 索引与模型版本、过滤器摘要、候选 ID/排名、降级原因 | 未授权候选正文 |
| 上下文 | Chunk ID、来源版本、截断与去重统计 | 完整敏感片段 |
| 生成 | Provider/模型/Prompt 版本、Token 用量、结束原因 | 密钥、完整 Prompt |
| 引用 | 引用 ID、定位校验结果、失效原因 | 用户无权访问的来源 |
是否允许采样正文取决于数据分类、用户授权和组织政策。确需采样时,应采用单独的受控存储、脱敏、短保留期和访问审计,而不是把正文混入普通应用日志。Prompt 注入内容和恶意文档同样可能含敏感信息,不应因为“用于调试”而跳过治理。
Trace 中可以建立 ingest、embed、retrieve、rerank、context_build、generate 等 Span,并记录错误类别与版本。OpenTelemetry 的 SDK、Exporter 和语义约定会演进,示例代码应按项目锁定的依赖版本验证,避免直接复制过时初始化方式。
5. 系统指标与质量指标要分开
系统指标包括请求量、错误、取消、各阶段耗时、队列积压、外部依赖限流、资源饱和和 Token 用量。
质量指标包括零召回、无答案、引用缺失或失效、权限拦截、负反馈、人工改写和固定评估集的版本变化。
HTTP 200 不代表回答正确,因此只监控 QPS、P95/P99 和 5xx 无法发现 RAG 质量退化。反过来,Judge 分数也不能解释连接池耗尽或 Provider 限流。两类信号应通过 request ID、配置版本和索引版本关联。
每条告警都要有阈值依据、负责人和处理动作。本文不提供统一数值:不同文档规模、模型、租户和交互方式的正常区间不同,应从压测、灰度和历史基线建立服务目标。
6. 部署前的兼容性检查
- 应用版本是否兼容当前 Chunk Schema、索引、缓存和引用格式。
- Embedding 或切片变化是否写入新索引版本,而不是原地混写不兼容向量。
- 权限更新是否覆盖向量索引、关键词索引、缓存和已生成的分享链接。
- Readiness 是否验证必要依赖,Liveness 是否避免因短暂外部故障造成重启风暴。
- 客户端取消和总超时是否传递到检索、重排和模型调用。
- 流式响应中断时是否停止上游工作并正确记录用量与结束原因。
- 数据库、索引和队列迁移是否可以前向兼容,是否有恢复演练。
7. 灰度、降级与回滚
发布单位不只是 Go 二进制,还包括 Prompt、生成模型、Embedding、切片、索引、检索参数、权限策略和引用格式。推荐流程是:
固定离线评估
-> 影子流量或内部验证
-> 小范围灰度
-> 同时观察系统与质量信号
-> 分阶段扩大或停止每一阶段都应预先写明扩大和停止条件。可用的降级方式取决于业务,包括关闭重排、切换到经过评估的检索路径、只返回搜索结果、禁用生成、进入人工队列或拒绝请求。降级不能绕过权限,也不能把“少了一路结果”静默标记为完整成功。
回滚前需保留事故证据,并确认目标组合与当前数据兼容。若新版本已经写入不同 Schema 或索引,应切换读别名或恢复兼容快照,而不是只回退容器。至少演练 Provider 不可用、索引构建失败、错误权限规则、流式中断和新版本质量退化。
8. 总结
Go 可以把 RAG 查询和索引任务接入容器、指标与追踪体系,但生产可靠性来自明确的超时、错误处理、权限、数据治理、质量评估和回滚设计,而不是语言本身。
- 构建可追溯:镜像、应用和配置版本能够对应到发布记录。
- 观测可关联:系统信号、质量信号、索引版本和请求证据能够串联。
- 数据可治理:日志、Trace、缓存和评估样本都执行权限与脱敏规则。
- 发布可逆:灰度、停止、降级和完整配置组合回滚经过演练。
回到 RAG 专题查看完整学习路径,先用 RAG 评估方法建立发布基线,再按 RAG 生产化检查清单完成上线评审。项目架构与权限、引用链路见 企业级 RAG 知识库。