2026年7月11日/ AI Application Notes

一次 LLM 调用后,到底该保存什么

把回答、会话、引用、检索 trace 和模型调用记录拆开保存,才能回答结果从哪里来、为什么会这样,以及如何复现。

AILLM APIAuditRAGPostgreSQL

第一次把 LLM 接进接口时,很容易只保存两样东西:用户问题和模型回答。页面能跑,聊天记录也有了,看起来已经完成。

但一旦有人问“这句话依据哪份资料”“为什么昨天能答、今天不能答”“这个答案到底用了哪个模型”“刚才那次为什么慢”,只存一段文本就完全不够。

我现在把一次 LLM 调用拆成三类记录:会话事实、证据 trace、运行审计。三者解决的问题不同,不能混在一张表里,也不能全塞给缓存。

先说结论:答案不是唯一需要保存的结果

以一个带知识库的问答接口为例,一次请求通常会产生这些东西:

用户问题
  -> 会话历史
  -> 查询改写
  -> 召回候选与重排结果
  -> 模型回答 / 拒答
  -> 引用片段
  -> 模型、耗时、错误和关联 ID

其中只有“模型回答”适合直接展示给用户。其余内容是为了让系统能解释、排障和复现。

我会优先保存下面三组数据。

记录 要解决的问题 最少字段
会话与消息 下一轮怎么接上上下文?服务重启后怎么办? conversationId、顺序号、角色、内容、创建时间
引用与检索 trace 这个答案基于什么?检索在哪一步出了问题? 查询、候选片段、引用、检索模式、耗时
运行审计 到底用什么模型跑的?是否重试、降级或越过预算? 请求 ID、模型、供应商、参数、状态、错误、耗时

会话:保存业务事实,不保存一段无边界的 history

SmartKB 的会话记忆放在 PostgreSQL,而不是 Redis。每次 Advanced RAG 成功返回后,服务将本轮用户问题和助手回答写入同一个 conversationId

chatMemory.add(conversationId, new UserMessage(question));
chatMemory.add(conversationId, new AssistantMessage(result.answer()));

下一轮不会让前端把整段聊天记录传回来,而是由服务端按照 conversationId 取最近有限条消息,用于查询改写。这样会话顺序、权限和清理策略都在后端控制,前端不会借一段伪造历史覆盖真实上下文。

消息记录至少需要这些字段:

conversation_id
sequence              # 同一会话内稳定递增
role                  # USER / ASSISTANT / SYSTEM
content
created_at

多实例下尤其要注意 sequence。它不是展示用的小细节:两台服务同时追加消息时,如果没有原子递增,后续读取出来的上下文顺序可能是乱的。SmartKB 的持久化会话实现会在写入前原子推进会话序号,再写入消息。

Redis 仍然有价值,但只适合放最近上下文这类可失效缓存。会话和消息是业务事实,缓存丢失后必须能从数据库恢复。

引用和检索 trace:回答之外还要知道“依据是什么”

RAG 的问题不只是模型会不会胡说。很多时候模型本身没有错,错的是召回到了不相关的片段、过滤条件漏了租户,或者重排把正确资料挤掉了。

因此,答案生成后应该保存检索侧的证据。SmartKB 的 RetrievalTrace 记录包含:

id
assistant_message_id
query
candidates_json
retrieval_mode
latency_ms
created_at

其中 assistant_message_id 很关键。它把用户看到的那一条回答,和当时实际参与判断的检索过程连起来。排查时不需要猜“当时的索引是不是变了”,直接就能看到:原问题是什么、改写后查询是什么、候选片段有哪些、用了哪种检索模式、耗时多少。

对外返回时不必暴露完整候选内容,但至少应该带可展示的引用:文档名、片段 ID、标题或页码。对内则保留更完整的候选集合,方便分析召回质量。

我不会把引用只拼进回答正文。引用应该是独立字段,因为前端需要展示它,后端需要校验它,评测也需要统计它。

{
  "answer": "已发货且超过售后时限的订单不能申请退款。",
  "references": [
    {
      "documentId": "refund-policy-v3",
      "chunkId": "chunk-17",
      "title": "退款与售后规则"
    }
  ],
  "traceId": "retrieval-trace-id"
}

运行审计:别只记一条“调用成功”日志

模型调用还需要一条独立的运行记录。它不等同于会话消息,也不等同于检索 trace。

OpsPilot 的诊断服务在最终报告里保留了供应商、模型名、工具调用数量、计划读取项、证据和假设;平台侧再用 correlationId 做幂等保存。这样同一请求被重复提交时,不会悄悄多写一份诊断结论。

一条适合审计的调用记录,我会至少包含:

request_id / correlation_id
conversation_id              # 没有多轮会话时可以为空
provider
model_name
prompt_version
temperature / max_tokens
started_at / finished_at / latency_ms
status                        # SUCCESS / REFUSED / FAILED / FALLBACK
input_tokens / output_tokens  # 供应商返回时再记录
error_code                    # 不保存完整敏感错误内容
trace_id

这里的 prompt_version 经常被忽略。提示词改一个约束就可能影响输出分布;模型别名也可能在网关侧更新。没有模型、参数和 prompt 版本,就无法比较改动前后的效果,更无法可靠回归。

OpsPilot 还把“证据不足”作为 needs_more_evidence 保存,而不是让模型给一个看起来完整的诊断。这种状态也应该进入审计,它说明系统做了受控拒答,不是一次普通失败。

什么不能原样保存

“都存下来,之后再说”通常会带来新的安全问题。至少要提前决定下面几类数据的处理方式:

  • API Key、Authorization、Cookie、数据库连接串等密钥,必须在入库和日志前脱敏或直接丢弃。
  • 原始文档、日志和工具返回可能含个人信息或业务敏感字段。需要按业务保留期限、访问权限和脱敏规则处理。
  • 用户输入不应被当作可信指令保存后再无边界地拼进未来 prompt。读取历史时仍要限制条数、长度和角色。
  • 失败响应不要只存异常堆栈。应保存可检索的错误码和关联 ID,敏感细节留在受控日志系统。

OpsPilot 的平台 API 会拒绝包含类似密钥字段的证据,并且只接受结构化诊断报告。这种“先校验再保存”的思路比事后清洗可靠得多。

我会怎样安排写入顺序

写入不是越早越好。一个比较稳妥的顺序是:

  1. 创建请求 ID,记录调用开始时间和已知配置。
  2. 读取会话和检索资料,但不要把未校验的原始输入当作系统指令。
  3. 调用模型并校验结构化输出。
  4. 生成最终业务响应,确定引用、拒答状态和错误边界。
  5. 在事务或可补偿流程中保存用户消息、助手消息、引用 / trace 和运行审计。
  6. 返回给前端时同时带上展示结果和必要的追踪 ID。

如果模型调用失败,运行审计仍然要落库;如果检索置信度不足,保存拒答原因和可用引用,但不把模型生成内容伪装成答案。这样后面看监控时,才能区分是模型网关问题、检索问题,还是业务数据本来就没有覆盖。

一个够用的检查清单

  • 会话、检索 trace、调用审计是否是三类独立记录?
  • 服务重启后,是否还能按会话 ID 恢复最近上下文?
  • 每条回答能否追溯到引用片段和本次检索?
  • 能否知道它使用了什么供应商、模型、参数和 prompt 版本?
  • 证据不足、模型失败和降级路径是否被显式保存?
  • 密钥、敏感字段和原始工具输出是否经过脱敏、权限和保留期设计?
  • 是否能用一个 requestIdcorrelationId 把接口日志、模型调用、数据库记录串起来?

真正把 LLM 接进业务以后,保存的目的不是囤更多数据,而是让每一次回答都能被解释。用户要看到答案,开发者要看到依据,系统要保留足够的上下文来处理下一轮,也要在出问题时能找回当时发生了什么。做到这一点,大模型能力才算真正进入了系统,而不是停在一个聊天框里。

Brief

An answer alone is not enough to explain an LLM-backed feature. Save the conversation, retrieval evidence and execution metadata as separate records.

Use a durable database for business facts; caches are allowed to disappear.

Redact secrets and define retention before persisting prompts, tool results or raw documents.