ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从RAG到Agent:上下文工程与向量数据湖的落地实践

从RAG到Agent:上下文工程与向量数据湖的落地实践

前一阵子帮一个做内部运维工具的团队做技术复盘,他们原本那套 RAG 问答系统上线大半年,文档检索体验一直挺稳。直到业务方提了一串新需求——“帮我看下这个工单为什么卡了一个小时没流转”“能不能直接列出这个变更影响了哪几条链路”——团队发现,光靠检索文档已经顶不住了。

这不是模型变笨了,而是我们把大模型用得太窄了。传统 RAG 把所有问题都当成“在文档里找答案”,可真正的 Agent 场景需要的是把文档知识、业务状态、工具能力、历史轨迹放在一起,动态拼出适合当前任务的一份上下文。这个拼装过程,现在有个更准确的名字——上下文工程。

这篇文章我想聊聊,从 RAG 升级到 Agent 的过程中,为什么上下文工程会成为核心瓶颈,以及当数据规模变大、关联变多的时候,为什么普通向量数据库不够用,要把向量索引放到数据湖的体系里去。文章会给出一个可以直接参考的落地管线,以及我在实际推进里踩过的几个坑。适合正在做 RAG 增强、或者正准备把问答系统往 Agent 方向升级的团队。

1. 从拼装上下文到构建上下文:RAG 为什么会被 Agent 逼到墙角

1.1 传统 RAG 的三个步骤,以及它最容易被忽略的天花板

标准 RAG 的流程可以压缩成三步:离线把文档切块、向量化、写入索引;在线用 query 去索引里召回 TopK;把命中的文本块拼进 Prompt,交给模型生成。

这个流程在“知识型问答”场景下确实非常能打。比如内部制度、产品文档、FAQ,问题基本是“某个流程是什么”“某个字段怎么填”,答案就藏在某个段落里,召回准确率稍微差一点,模型也能靠理解能力兜底。

但一旦问题从“是什么”变成“为什么我的订单卡住了”“这个任务现在卡在哪一步”,RAG 就开始露怯。我见过最多的情况是:系统成功检索到了退款政策文档,却没有去查订单状态;检索到了任务操作手册,却没有看任务实际流转日志。因为传统 RAG 的索引里存的是文档切片,检索条件只有一个 query,它根本没有能力把结构化的业务状态拉进来。

更深一层的问题在于分块。很多团队为了让 embedding 效果稳定,喜欢固定 512 个 token 切块。但一个完整业务流程经常被切成两截,前半截在讲条件,后半截在讲例外,模型只捞到后半截就很容易答错。这是切块粒度与语义完整性之间的经典矛盾,也是传统 RAG 第一个要被 Agent 打破的围墙。

1.2 Agent 真正缺的不是“知识”,而是“状态、行动、上下文”

Agent 和 RAG 最大的区别在于目标不同。RAG 的目标是“回答一个问题”,Agent 的目标是“完成一个任务”。完成任务意味着需要拆解步骤、调用工具、读取状态、维护中间变量、在失败时重试。

这些能力对应到模型输入上,就变成对上下文的四种新要求:

  • 状态上下文:当前任务进行到哪一步,哪些变量已经从接口拿回来了,哪些还在等;
  • 工具上下文:哪个工具可用,入参出参的 schema 是什么,校验规则是什么;
  • 记忆上下文:这个任务里已经执行过哪些动作,出现过的关键信息是什么;
  • 知识上下文:从文档和资料中检索到的证据片段。

传统 RAG 只具备第四类,而且连第四类都做得比较浅。Agent 要求系统一次性把四类上下文按合理的优先级组装起来,还不能超出模型的窗口限制。这就是“上下文工程”真正要解决的问题。

换个生活化的类比:如果把 Agent 比作一个新员工,知识库只是书架,上下文工程则是带教导师在开会前提前整理的“背景材料包 + 待办清单 + 已有进度 + 可用工具”。新员工厉害不假,但你只给他一堆书,他连公司当前的项目状态都不知道,再强的推理能力也发挥不出来。上下文工程就是负责把这份材料包准备得恰到好处的那个角色。

2. 向量数据湖:普通向量数据库承接不住 Agent 的上下文

2.1 向量数据库解决了“快”,但没有解决“齐”和“活”

很多人问我,Agent 上下文一多,直接用向量数据库不行吗?数据量小、业务单一的场景下,完全可行。可一旦业务复杂起来,向量数据库的三个短板就会显露出来。

第一是“不齐”。向量数据库擅长存文本块,却不太擅长表达“实体 - 关系 - 状态”这种结构。比如一个工单,它的状态在业务表里,备注在另一个日志流里,同类故障的知识又散在文档库里,Agent 要的是三者的关联结果,而向量数据库只能给你其中一类。你当然可以硬把所有东西都拼成一个文本再向量化,但只要状态一变,整个文本就要重新嵌入,代价很高。

第二是“不活”。向量索引的更新通常很重,文档改了或者状态变了,往往要重建索引。业务状态是分钟级变化的,比如“工单当前等待审批”和“工单已经审批通过”,只差一个字段,但语义完全不同。全量重建根本跟不上这种频率,结果就是 Agent 拿到了一个已经过期的“状态快照”。

第三是“不透明”。向量数据库几乎没有治理概念,数据从哪来、血缘是什么、过期没有、谁改的,都不好追溯。Agent 场景一旦出问题,你连“模型当时到底看到了什么”都说不清。这个成本在调试阶段会被无限放大。

2.2 向量数据湖怎么和上下文工程结合

向量数据湖不是让你抛弃向量数据库,而是一种更底层的数据组织方式。所有数据先进湖,湖里存两条路:一条是结构化字段,一条是向量字段。二者共用同一套表结构、同一套权限、同一套更新机制。

打个比方:向量数据库像一个小书柜,所有文档按相似度排列,找书很快,但书柜不知道这本书是谁买的、借给谁了、库房里还剩几本。向量数据湖更像一个有库存系统的仓库:进货、入库、质检、上下架是标准流程,书架只是出货前的临时摆放点。你关心的是整条供应链,而不是最后那一下的摆放顺序。

技术形态上,可以用数据湖表格式存储加向量列实现,再配一个异步向量索引构建任务。新数据到达湖里后,由更新任务计算 embedding,写进向量索引;业务状态字段变更时,即使 embedding 不变,也能用同样的实体 ID 把最新状态带进上下文装配阶段。这个设计的关键在于:向量只是入口,真正的上下文来源是结构化状态和实体关系。

2.3 上下文服务的四层结构:存储、索引、装配、投递

把上下文工程当成一个系统来看,我习惯拆成四层:

  • 存储层:统一承接文档、业务实体、日志、工具结果,保留历史版本。这一层解决“有没有”的问题;
  • 索引层:同时提供向量索引、全文索引和 Metadata 过滤,按业务域分 partition。这一层解决“找得到”的问题;
  • 装配层:根据 Agent 当前的任务和实体 ID,决定检索什么、拼什么、拼多少。这一层解决“拼得好”的问题;
  • 投递层:把装配结果按 Token 预算裁剪、排序、格式化成模型友好的结构。这一层解决“喂得下”的问题。

这四层里面最容易被忽略的是装配层。很多 RAG 项目直接跳到“检索 + 拼接”,跳过了“规划”。正确的做法是:先判断任务属于哪一类——查事实、做操作、做对比、跟进度——再决定要引用哪几类来源,最后才是执行检索和拼装。装配层没有想清楚,下面的存储和索引建得再好,出来的上下文也是一锅乱炖。

3. 上下文工程落地:一条可复用的完整实现管线

3.1 数据接入:先解决实体统一,再谈向量化

我用一个非常常见的运维工单场景来走完整条管线。假设 Agent 要做三件事:解释工单是什么问题、判断影响范围、给出处理建议。这三件事分别需要:工单描述和备注(文本)、资产关系数据(结构化)、历史相似工单(向量检索)。

第一步不是建索引,而是定实体。所有数据到湖里时,都统一打上 entity_id(比如工单号),再带上 event_time、source_type。工单、日志、资产、知识库文档,全部靠这个公共 ID 关联。这一步做不好,后面所有多源上下文都是空中楼阁,因为检索出来两块数据你根本不知道它们是不是在说同一件事。

实际做法是:先画一遍“任务需要哪些数据”,再为每一份数据确定它挂在哪个实体下。比如“判断影响范围”需要资产表和变更记录,那就让变更记录也带上资产 ID;“给出处理建议”需要知识库和相似工单,那就让历史工单也带上故障类型标签。实体统一之后,向量化才有意义。

3.2 向量化和索引策略:分块、模型、索引参数怎么配

分块建议按语义边界而不是固定长度。我的常用做法是:先按标题和段落切,超过阈值再二次切。块长设在 256 到 512 token 之间,overlap 设 32 到 64。这里面的取舍很直接:块越大,单块信息越完整,但召回精度越差,也越占 Token 预算;块越小,精度上去了,但语义可能被切断,模型要靠前后块才能理解完整逻辑。

Embedding 模型选型上,中文场景优先选中文效果好的通用模型,维度在 768 到 1024 左右比较均衡。不要一上来就选超大模型,向量维度和检索耗时、存储成本直接挂钩。如果你同时要存文本向量和配图向量,建议分开建索引,不要混在一个向量列里,否则相似度计算会被没意义的维度干扰。

索引参数方面,HNSW 这种图索引可以设 M=16、efConstruction=200。召回阶段用 es 相对高的配置保证召回率,拿到 TopK 结果后再做一次重排序,从 20 条里筛出 5 条真正有用的。索引按业务域分 partition,比如工单域、知识域、资产域分开,冷热数据也分开,历史工单可以降到低频存储,避免拖慢在线检索。

3.3 装配器核心逻辑:路由、检索、拼装、预算控制

装配器是整个上下文工程的中枢。核心流程可以用一段伪代码来描述:

def assemble_context(task, entity_id, state, history): plan = route(task) # 判断任务类型:查证型/操作型/跟进型 ctx = {} # 1. 拉取结构化状态,永远先于文本 ctx["state"] = fetch_state(entity_id) # 2. 根据状态决定检索 query,而不是用用户原文 queries = plan.build_queries(task, state) hits = vector_search(queries, top_k=20) hits = rerank(hits, task, top_k=5) # 3. 调用工具并入参出参校验 ctx["tool_schema"] = fetch_tool_schema(plan.tools) ctx["tool_result"] = call_tool(plan.tools, entity_id) # 4. 压缩历史轨迹,控制长度 ctx["memory"] = summarize_history(history, max_tokens=500) return budget_control(ctx, max_tokens=2000)

这个流程里有两个关键技巧。一是 query 改写:用户问“工单为什么卡着”,装配器解析出“status=pending 且等待时长超过 N 小时”,用这个结构化条件去过滤,而不是拿原话去全文检索。二是 budget_control:先分配硬预算,比如系统指令 500 token、状态 200、工具 schema 300、证据 800、记忆 200,超出后从低优先级砍起,优先保状态和证据。

预算控制的优先级顺序很重要。我的经验是:结构化状态永远排在第一位,因为它是最新的事实;其次是工具返回结果;再次是记忆摘要;检索证据排最后。原因是检索证据的错误容忍度最高,模型能对证据做一定程度的纠错,但状态和工具结果是硬事实,错了就是错了。

3.4 反馈闭环:让上下文越用越准

最后一步是把 Agent 的执行结果回收进湖里。用户采纳了处理建议,就把这条完整上下文打上“effective”标签,作为未来分块和检索的正样本;用户改了答案,就把改写后的结果作为负样本。这些样本攒多了,就可以做一次分块或者检索策略的回测。

比如发现某类问题经常因为“缺少上一步的审批状态”而失败,就可以在装配器里为这类任务强制插入状态字段,而不是去改 Prompt。这个机制比调 Prompt 有效得多,因为它是改在数据链路层面,而不是指望模型自己理解。

每两周建议做一次批量回放:收集这段时间的生产失败案例,用当前的装配器重跑一遍,看失败率有没有下降。如果没有,就去检查是不是某些分块一直没更新,或者检索策略已经跟不上业务变化。把反馈闭环跑起来,上下文工程才算真正“活了”。

4. 常见问题与排查实录:四个高频坑的定位与解法

4.1 召回结果看着相关,模型就是不采纳

这类问题的现象非常有迷惑性:用相似度排序时,前几条结果肉眼看着“沾边”,但放在一起之后逻辑断裂,模型总回一句“根据现有信息无法确认”。我排查过好几个案子,最后基本都落在同一个根因上:分块边界切断了因果链条,或者关键词语义和业务语义没有对应上。

我的处理方法是:开离线批测,把所有失败案例的上下文快照拉出来,逐个看“模型当时读到的到底是哪几块”。解决手段通常有三个:调分块策略,改按标题和段落语义切;加规则过滤,把与任务实体无关的块直接丢掉;引入重排序模型,在 TopK 基础上按任务相关性重新排序。重排序能提升准确性,但会引入额外延迟,高频场景要评估清楚再上。

4.2 上下文太长,Token 预算很快就爆炸

这个坑几乎每个人都会踩。原因很简单:装配器觉得“多给点总比少给好”,于是把能搜到的证据全塞进去。结果模型窗口很快就满了,还出现“长篇大论把关键状态淹没”的现象。

建议先建一个预算模型。比如总窗口是 8k,模型输出至少留 1.5k,那么上下文可用的就是 6.5k。系统指令固定 1k,工具 schema 和状态各 0.5k,剩下的 4.5k 才轮得到证据和记忆。证据进入之前先过一道评估:如果任务需要对比多个工单,证据可以放多点;如果只是查单一状态,证据上限控制在 1.5k 就够。设置完预算之后,用影子模式跑两周,对比是否出现“上下文缩短但效果反而变好”的情况,这种情况我遇到不止一次。

4.3 数据已经变了,Agent 还在用旧上下文

曾经有个场景:用户已经在界面上看到“工单状态已更新”,但 Agent 检索出来的还是变更前生成的文本快照。原因是文本快照在工单状态变化时没有同步触发重建,而检索索引里存的是旧版本。

排查时先看数据管线:业务库到湖、湖到向量索引、索引再到装配器,每一段都有缓存和延迟。我的经验是给每一条上下文记录加上 version 和 timestamp,装配时拿着实体 ID 去校验最新状态;如果状态变了但文本快照还没更新,宁愿先用结构化状态字段,也不要拿旧文本凑数。等异步更新任务把新快照构建好之后,再切换回文本。

4.4 上下文看不到摸不着,复盘全靠猜

这是我在所有 RAG 和 Agent 项目里最推荐优先做的事情:全链路 Trace 加上下文快照。每次 Agent 执行时,把路由结果、检索 query、命中块 ID、最终上下文全文、模型输出一起存下来,固定成一个统一的记录结构。出问题之后,拿 Trace 做回放,很快就能定位是检索、装配还是模型理解出了问题。

下面这张速查表是我自己整理的,遇到类似问题可以直接对着排查。

表象深层原因排查路径解决方案
召回结果看似相关但模型不采纳分块割裂语义、证据与任务实体脱钩查看上下文快照,定位命中的块按语义边界切分、加规则过滤、引入重排序
上下文过长,Token 预算爆炸没有预算控制和优先级排序统计各类上下文的占比预算模型加优先级裁剪
旧数据反复出现链路有缓存且没有版本校验检查各段延迟和缓存策略上下文带 version 和 timestamp,状态变更优先
问题无法复现,改 Prompt 全靠猜没有保存上下文快照查看 Trace 日志回放执行过程全链路 Trace 加快照存档

5. 衡量上下文工程做得好不好:从离线指标到线上指标

5.1 离线指标先跑通:召回率、MRR 和上下文利用率

上下文工程还不能只看最终效果,因为模型能力会掩盖很多问题。我习惯拆成两个层次评估。

第一层是检索质量。按业务域构建评估集,每个任务标注“应该召回哪些实体、哪些文档段”,然后计算召回率和 MRR。这个指标主要反映分块和向量化策略是否合理。

第二层是上下文利用率。统计最终上下文里有多少 Token 真正被模型引用。上下文利用率等于被引用的证据块数除以总证据块数。如果检索回来 5 块,模型只引用了 1 块,大概率是召回策略太宽或者证据与任务脱钩。这个指标比对话流畅度客观得多,因为它完全建立在可统计的事实上。

5.2 线上指标紧盯端到端任务成功率

更贴近业务的指标是端到端任务成功率。给每个任务定义一个可验证的完成条件,比如“工单状态被修改且变更记录写入成功”算一次成功,“给出了建议但用户没采纳且继续追问”算失败。这个指标的好处是绕过“对话是否流畅”这种主观评价,直接说明上下文工程有没有帮 Agent 把事办成。

如果端到端成功率上不去,先不要急着调 Prompt,优先去检查上下文快照里有没有关键状态缺失。我遇到过不少案例,表面上像是模型回答不对,实际是装配器压根没把“当前审批人”放进上下文,模型只能靠猜。

5.3 不要只看单点指标,要用一把尺子做基线

不同团队、不同场景,离线指标往往没有可比性。我的做法是固定一个任务集合和一个成功率计算口径,把它作为团队内部的“基线尺子”。任何新方案,先拿旧方案跑出基线,然后新方案必须百在成功率和延迟上同时不劣于基线,才允许灰度。

这套评估体系看着简单,但非常有必要。上下文工程一旦做起来,随之而来的就是大量的参数调整和方案对比。没有一把固定的尺子,你根本没法定性判断某个改动到底是变好了还是变差了。把离线指标和线上指标配合着看,才能把每一次调整的收益量化出来。

结合我自己的体会,上下文工程做得好不好,最终要看你面对失败案例时,是胸有成竹地知道它坏在哪一环,还是满头雾水地反复调 Prompt。向量数据湖给的是地基,装配器给的是大脑,而真正让这套系统持续变好的,是每一份被记录下来的上下文快照,以及每次失败案例带来的策略修正。再往后的扩展方向,大概率就是把这些快照变成自动化的评估集,让装配器具备自我进化的能力,但那是另一个更长的故事了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进