ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从Demo到生产:RAG、记忆、API、MCP与鉴权审计的工程化实践

从Demo到生产:RAG、记忆、API、MCP与鉴权审计的工程化实践 1. 从能跑通到敢上线这套应用到底在解决什么问题大模型应用最尴尬的阶段不是跑不通而是能跑通但不敢给人用。我自己就经历过这个阶段本地写个脚本把文档塞进向量库接上模型 API问几句答得挺像样兴冲冲拿给同事试结果第一句话就把我问住了——这东西谁都能问吗问完的记录在哪那一刻我才意识到一个能演示的 Demo 和一个能交付的应用之间隔着的不是模型能力而是上下文管理、工具调用、身份鉴权和操作审计这四件事。这篇要聊的就是把这四件事串成一条完整链路用RAG解决模型不知道我们内部资料的问题用记忆机制解决多轮对话记不住、跨会话接不上的问题用API把模型能力封装成可被业务系统调用的服务用MCP把外部工具数据库、文件系统、内部服务标准化地接进来最后用鉴权与审计把整条链路管起来让每一次提问、每一次工具调用、每一次数据访问都有身份、有权限、有记录。适合谁看如果你已经能跑通一个最简 RAG Demo但卡在怎么让它像个正经系统这一步这篇就是给你写的。如果你还没碰过 RAG建议先补一下检索增强的基础概念再回来看工程化部分不然容易在细节里迷路。全文我会按为什么这么设计—怎么落地—踩过什么坑的顺序展开尽量把每个选择的理由讲透而不是甩一堆配置让你抄。先给一个整体心智模型后面所有章节都围绕它展开能力层解决的问题关键技术点典型失败表现RAG 检索层模型不知道私有知识切分、向量化、召回、重排答非所问、胡编引用记忆层多轮/跨会话上下文丢失短期窗口、长期摘要、半衰期衰减反复问同样的事、前后矛盾API 服务层能力无法被业务调用接口设计、流式返回、错误码401/400 频发、超时无兜底MCP 工具层模型无法操作外部系统协议封装、工具注册、权限边界工具乱调、越权访问鉴权审计层谁用了、干了什么不清楚身份认证、权限校验、日志留痕无法追溯、无法定责这张表不是理论是我踩坑之后倒推出来的。下面逐层拆。2. RAG 检索层决定回答质量的上限而不是模型很多人把 RAG 当成把文档丢进去就完事结果上线后命中率惨不忍睹。我一开始也这么想直到发现同一个问题换个切分方式答案质量天差地别。RAG 的本质是用检索结果约束模型的生成范围检索不准后面再强的模型也救不回来。2.1 切分策略为什么固定长度切分几乎总是错的最省事的做法是按固定字符数切比如每 500 字一段。这个方案在纯叙述性文档上勉强能用但只要文档里有表格、代码、层级标题就会把语义切碎。我实测过一个内部规范文档按 500 字硬切之后配置项 A 的取值范围和配置项 A 的默认值被切到了两个块里模型召回其中一块就答不全。我的做法是结构化优先、长度兜底先按 Markdown 标题层级切标题作为块的元数据保留如果某个标题下内容超过阈值我一般设 800 到 1200 字再按段落二次切分并给每个子块带上父标题路径。这样召回时既能命中细节也能通过父标题补全上下文。# 结构化切分的核心逻辑示意 def split_by_structure(doc, max_len1000): blocks [] for section in parse_markdown_sections(doc): if len(section.content) max_len: blocks.append({ text: section.content, path: section.title_path, # 如 配置 网络 超时 }) else: for para in split_by_paragraph(section.content, max_len): blocks.append({text: para, path: section.title_path}) return blocks提示切分长度没有万能值。中文技术文档我一般用 800 到 1200 字英文偏 500 到 800 词。判断标准很简单——切完之后随便抽一块你自己能不能只看这块就理解它在说什么能就合格。2.2 召回与重排为什么只做向量召回不够向量召回擅长语义相似但对精确匹配比如型号、编号、专有名词很弱。我遇到过用户问错误码 401 怎么处理向量召回返回了一堆讲权限的段落就是没返回那个明确写着 401 的表格。解决办法是混合召回向量召回 关键词召回BM25 之类两路结果合并后再用重排模型精排。重排这一步很多人省掉觉得多花钱。我的经验是重排对最终质量的影响比换更大的生成模型还明显。因为召回阶段为了不漏通常会取 Top 20 甚至 Top 50里面噪声很多重排负责把真正相关的 3 到 5 条顶上来。生成模型看到的上下文干净了幻觉自然少。阶段目标常用手段我的参数经验召回尽量不漏向量 关键词双路每路 Top 20合并去重减少冗余按内容哈希去重合并后约 30 条重排把对的顶上来交叉编码重排模型保留 Top 5生成基于证据作答带引用约束的提示词强制标注来源块2.3 引用与拒答让模型不知道就说不知道RAG 最危险的不是答错是答错还编得像真的。我在提示词里强制要求每个结论后面标注来源块编号如果检索结果里没有支撑必须回答根据现有资料无法确认。这条规则加上去之后拒答率上升了但用户信任度反而高了——因为大家发现它说不知道的时候是真不知道说知道的时候基本靠谱。这里有个细节拒答判断不能只靠模型自觉。我会在检索阶段加一个相关性阈值如果重排后的最高分低于阈值直接走拒答分支不把噪声喂给模型。阈值需要按你的数据调我一般从 0.3 左右开始试看一批标注问题的命中情况再微调。3. 记忆层短期窗口、长期摘要与半衰期衰减多轮对话记不住是用户抱怨最多的问题之一。但记住这件事没那么简单——全记住会撑爆上下文全不记又像失忆。我的方案是分三层短期窗口保最近几轮原文长期记忆存摘要和关键事实再用一个衰减机制决定哪些旧信息该淡出。3.1 短期记忆窗口不是越大越好最直接的做法是把最近 N 轮对话原文都塞进上下文。N 取多少我试过 10 轮、20 轮发现超过 10 轮之后模型对早期内容的注意力明显下降而且 token 成本涨得很快。更麻烦的是早期对话里如果有已经被纠正的错误信息模型可能又被带偏。我的做法是滑动窗口 关键轮次标记默认保留最近 6 到 8 轮但如果某一轮里出现了用户明确确认的事实比如我的项目用的是 Python 3.11就给这轮打标记让它优先保留。这样既控制了长度又不丢关键信息。3.2 长期记忆摘要 结构化事实跨会话的记忆我不用原文存而是存两类东西一是会话摘要把一段对话压缩成几句话二是结构化事实比如用户偏好、项目配置、常用参数存成键值对。下次会话开始时把相关的事实和摘要注入系统提示词。# 长期记忆的存储结构示意 memory_item { user_id: u_1024, type: fact, # fact 或 summary key: preferred_language, value: Python 3.11, created_at: 1717000000, last_hit_at: 1717003600, hit_count: 7, }为什么要分 fact 和 summary因为它们的检索方式不同。fact 适合精确匹配用户问我用什么语言直接查键summary 适合语义召回用户问上次我们聊到哪了。混在一起存检索效率会下降。3.3 半衰期衰减让旧记忆自然淡出记忆不是越多越好过期的信息会干扰判断。我借鉴了记忆 分数 时间半衰期这个思路每条记忆有一个基础分随着时间推移按半衰期衰减被命中时分数回升。检索时按当前分数排序低分的自然沉底。记忆类型基础分半衰期命中回升用户明确确认的事实1.030 天0.3会话摘要0.67 天0.2模型推测的信息0.33 天0.1这个表是我调出来的经验值不是标准答案。核心逻辑是用户亲口确认的比模型猜的可靠近期的比远期的相关。半衰期让系统自动遗忘省去了手动清理的麻烦。注意衰减参数一定要可配置。不同业务对记忆新鲜度的要求差别很大客服场景可能 7 天就够个人助理场景可能要 90 天。写死参数后期改起来很痛苦。4. API 服务层把能力封装成别人敢调的服务Demo 阶段大家都是本地跑脚本一旦要给前端或其他系统调用就得封装成 API。这一步看着简单坑却不少——我自己就被 401 和 400 折腾过好几轮。4.1 接口设计流式返回与错误码规范大模型生成慢如果等全部生成完再返回用户会以为卡死了。所以流式返回是必须的用 SSE 或者分块传输让前端能边收边显示。但流式带来一个新问题错误怎么传如果已经开始返回内容了才出错HTTP 状态码已经发出去了改不了。我的做法是双层错误处理请求进入时先做参数校验和鉴权这阶段的错误直接返回标准 HTTP 状态码401、400、403一旦开始流式输出后续错误通过流内的事件类型传递比如发一个event: error的数据块前端据此提示。// 流式接口的错误事件示意 // 正常数据块 // data: {type:delta,content:根据资料...} // 错误数据块 // data: {type:error,code:UPSTREAM_TIMEOUT,message:模型服务超时}4.2 那些让人抓狂的 401 和 400热词里频繁出现unexpected status 401 unauthorized: incorrect api key provided这个我太熟了。401 基本就三类原因密钥没传、密钥传错、密钥过期。排查顺序我固定为先看请求头里 Authorization 字段格式对不对是不是少了Bearer前缀再看密钥本身有没有多余空格或换行最后确认密钥在服务商那边是否还有效。400 里最常见的是上下文超长比如maximum context length is 1048576 tokens。这个报错的意思是你塞进去的内容超过了模型上限。解决办法不是简单截断而是在组装上下文时就做预算控制给系统提示词、记忆、检索结果、用户输入各分配一个 token 预算超了就按优先级裁剪。我一般把检索结果设成可裁剪的因为它是锦上添花而系统提示词和用户输入是必须保留。错误码常见原因排查动作401密钥缺失/错误/过期查请求头格式、密钥有效性400上下文超长、参数非法查 token 预算、参数类型403权限不足查角色配置、资源归属429触发限流查调用频率、加退避重试5xx上游服务异常查服务商状态、加重试兜底4.3 重试与降级别让一次抖动毁掉体验上游模型服务偶尔抖动是常态。我的策略是指数退避重试 降级兜底429 和 5xx 自动重试间隔按 1s、2s、4s 递增最多三次三次都失败就降级到备用模型或者返回一个友好的服务繁忙请稍后重试。关键是重试要有上限不然会把上游打得更惨。5. MCP 工具层让模型安全地操作外部系统MCP 这两年热度很高但很多人对它的理解停留在又一个协议。我的理解是MCP 解决的是工具接入的标准化问题。以前每接一个外部系统就要写一套适配代码现在按 MCP 的规范封装一次任何支持 MCP 的客户端都能用。5.1 MCP 到底解决什么从每个工具写一遍到写一次到处用举个具体场景我想让模型能查数据库、读文件、调内部接口。传统做法是给每个能力写一个函数然后在提示词里描述怎么用。问题是不同模型的函数调用格式不一样换个模型就得改一遍。MCP 把这些能力抽象成标准的工具和资源客户端负责发现和调用服务端只管实现两边解耦。这和软件协议还是硬件协议的疑问其实是一回事——MCP 是应用层的通信规范跟硬件无关你可以把它理解成工具调用的通用插座。插头客户端和插座服务端按同一标准做就能即插即用。5.2 工具注册与权限边界别让模型拿到万能钥匙工具接得越多风险越大。我见过有人把数据库的增删改查全暴露给模型结果模型一个误操作把数据改了。我的原则是最小权限 显式确认查询类工具可以直接调写入类工具必须经过人工确认删除类工具默认不开放。# 工具注册时的权限标注示意 tools [ {name: query_orders, level: read, confirm: False}, {name: update_status, level: write, confirm: True}, {name: delete_record, level: danger, confirm: True, enabled: False}, ]confirmTrue的工具模型调用时不会直接执行而是返回一个待确认的请求由前端弹窗让用户点确认。这一步多花几秒但能避免绝大多数误操作。5.3 工具调用的可观测性每次调用都要能复盘工具调用出问题时最难的是定位——是模型选错了工具还是参数传错了还是工具本身报错我的做法是全链路记录每次调用记录工具名、入参、出参、耗时、调用者身份、关联的会话 ID。这样出问题能直接回放。记录字段用途示例trace_id串联一次请求的所有调用tr_8f3a...tool_name定位是哪个工具query_ordersparams复现入参{user_id:u_1024}result看返回是否异常{count:3}duration_ms排查性能瓶颈245caller追溯调用者u_10246. 鉴权与审计让每一次访问都有身份、有记录前面四层解决的是能不能用这一层解决的是敢不敢用。没有鉴权和审计前面做得再好也不敢上线。6.1 身份认证从 API Key 到细粒度权限最简单的鉴权是发一个 API Key但 Key 一旦泄露就等于全权限。我的做法是Key 角色每个 Key 绑定一个角色角色决定能访问哪些资源、能调用哪些工具。比如只读角色只能查不能改管理员角色才能调危险工具。# 权限校验的核心逻辑示意 def check_permission(role, action, resource): policy ROLE_POLICIES.get(role, {}) allowed_actions policy.get(resource, []) return action in allowed_actions这样即使 Key 泄露攻击者能做的也有限。再配合 Key 的定期轮换和异常调用告警安全性会好很多。6.2 审计日志记录什么、怎么存、存多久审计日志不是把什么都记下来就行记太多查不动记太少查不到。我关注四类事件登录鉴权、数据访问、工具调用、配置变更。每条日志包含时间、身份、操作、对象、结果、来源 IP。存储上我建议冷热分离最近 30 天的日志放可快速检索的存储里方便排查更早的归档到低成本存储满足合规留存要求。日志本身要防篡改至少做到只追加不修改。提示审计日志里千万别记敏感明文比如完整的密钥、用户隐私数据。该脱敏的脱敏该哈希的哈希否则日志本身就成了泄露源。6.3 异常检测从日志里发现不对劲日志堆着不看等于没记。我会设几条简单规则做实时告警同一身份短时间内大量调用、非工作时间访问敏感资源、频繁触发权限拒绝、工具调用参数异常。这些规则不需要多复杂能覆盖常见异常就够。异常模式触发条件处置动作高频调用1 分钟超 100 次限流 告警越权尝试连续 5 次权限拒绝临时冻结 通知敏感时段非工作时间访问核心数据记录 复核参数异常工具入参超出正常范围拦截 告警7. 把五层串起来一次完整请求的旅程讲了这么多层最后用一个具体请求把它们串起来你会更清楚每层在哪发力。假设用户问帮我查一下上个月订单里金额最高的那笔顺便看看客户是不是老客户。鉴权层先校验请求携带的 Key 和角色确认这个用户有订单查询权限。记忆层加载该用户的长期记忆发现他之前提过关注大额订单把这个偏好注入上下文。RAG 层检索订单金额规则老客户定义等相关文档块重排后取 Top 5。API 层把系统提示词、记忆、检索结果、用户问题组装成请求做 token 预算控制后发给模型。模型判断需要调用工具通过MCP发起query_orders调用。工具层校验权限只读允许执行查询返回结果。审计层记录这次调用的完整链路谁问的、调了什么、返回了什么、耗时多少。模型基于工具返回和检索结果生成回答API 层流式返回给前端。这一趟走下来每个环节都有身份、有权限、有记录。这就是能上线和能演示的区别。8. 几个我踩过、你大概率也会踩的坑最后分享几个实操中印象最深的教训都是文档里不会写的。第一个坑以为 RAG 效果差是模型不行。我一开始换了好几个生成模型效果提升有限。后来发现问题在切分和重排把这两块优化之后同一个模型效果翻倍。先优化检索再考虑换模型这个顺序能省很多钱。第二个坑记忆存了但没清理。早期我把所有对话原文都存下来当记忆几个月后检索变得又慢又不准因为大量过期信息在干扰。加上半衰期衰减和定期归档之后才恢复正常。记忆要有进有出只进不出迟早撑爆。第三个坑工具权限一刀切。图省事给所有工具开了全权限结果测试时模型误调了一个写操作把测试数据改了。后来改成最小权限 写入确认再没出过事。权限这东西出事之前觉得麻烦出事之后觉得真香。第四个坑审计日志记了但没人看。日志堆了几十万条真出问题时翻了半天。后来加了实时告警规则异常主动推送到值班群排查效率完全不一样。日志的价值不在记在于用。这套架构我前后迭代了大半年从最初一个脚本到现在能支撑内部几十人日常使用。它不是最优解但每一步的取舍我都想清楚了为什么。如果你正在搭类似的东西建议先把鉴权和审计这两层想明白再动手因为它们决定了你的系统能不能真正交出去。至于 RAG 和记忆的调优那是上线之后持续打磨的事急不来。
返回列表