多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Agent记忆系统重构:从向量库翻车到混合检索的工程实践

Agent记忆系统重构:从向量库翻车到混合检索的工程实践 先交代一个背景我最近在做一个长期运行的对话 Agent一开始图省事把用户偏好、历史对话、工具调用记录全部切块、embedding、塞进向量数据库觉得这样就有了记忆。结果跑了两个星期问题一个接一个冒出来召回结果张冠李戴、旧需求反复污染新上下文、用户明确说换方案之后老方案还是阴魂不散。最后我不得不把架构推倒重来也彻底想明白了一件事——Agent Memory 不能只靠向量数据库它应该是一个由多种存储和检索机制组合出来的系统。这篇文章就是那次重构的完整复盘。我会从翻车现场讲起说清楚向量检索的能力边界再把认知科学里的 Atkinson-Shiffrin 记忆模型映射到工程架构上最后给出一个可落地的混合检索实现以及我在 Qdrant、Milvus、Redis 这些存储选型上踩过的坑。适合正在设计 Agent 记忆模块、或者被上下文永远不够用折磨的开发者参考。1. 把 Agent 的全部记忆塞进向量库之后我翻车了1.1 第一次全量向量化的结果上下文互相污染我的第一个方案很简单粗暴所有需要记住的内容——用户说过的话、Agent 自己总结的结论、工具返回的结果——按固定长度切块经过 embedding 模型变成向量写入向量数据库。每次对话开始前把用户当前的问题也 embedding 一下做 top-k 相似度检索把命中的记忆块拼进 system prompt。听起来很顺理成章对吧第一周确实能用但很快暴露出一个致命问题记忆之间互相污染。举个具体场景。用户在 3 月 10 日让我帮他调研云厂商的定价我当时生成了一个对比表格结论是A 厂商性价比最高。3 月 15 日用户又说我们决定用 B 厂商因为老板喜欢他们的售后。这两段记忆在向量空间里高度相似——都在讲云厂商、定价、选型。于是后续任何关于云厂商的提问都会同时召回这两段内容。Agent 有时会说出根据您之前的对比A 厂商更合适这种话直接激怒用户。更麻烦的是向量检索没有版本概念。同一个话题用户改了三次需求向量库里就存着三个版本的记忆它们彼此相似、互相干扰但检索时无法判断哪条是最新的、哪条已经被用户明确否决。这还只是污染问题后面还有更深的坑。1.2 排查过程为什么相似度检索会把记忆搅成一锅粥当时我第一个反应是是不是 embedding 模型不够好于是换了好几个模型反复实验发现结果大同小异。真正的原因不是模型而是相似度语义和记忆有效性根本是两回事。我做了个实验把用户说过的 1000 条历史记录全部检索一遍人工标注每一条召回结果是否有用。数据是这样的召回情况占比典型例子语义相关但时效过期32%用户已经放弃的旧方案语义相关但对象错误21%项目 A 的结论被用于项目 B真正有效的记忆27%用户偏好、明确决策完全不相关20%纯噪音也就是说纯靠向量相似度真正有效的命中率不到三分之一。原因在于向量检索天然只做语义匹配它不知道这条记忆是什么时候产生的、属于哪个项目、用户后来有没有推翻它、它的重要程度如何。这些信息全部要靠额外的元数据来承载而元数据恰恰是我当时完全没有设计的。1.3 我总结出的核心结论记忆系统不是检索系统那次翻车让我明白一个道理Agent Memory 是一个存储系统 生命周期管理系统 检索系统的综合体向量数据库只是其中检索环节的一种索引方式。存储层面要区分短期的会话上下文、长期的事实偏好、程序性的操作流程生命周期层面要做写入、合并、衰减、遗忘、冲突解决检索层面要结合语义召回、关键词精确匹配、规则硬过滤最后还要有重排。这些东西单靠一个向量库根本撑不起来。2. 向量数据库擅长什么不擅长什么2.1 向量检索引擎的本质是语义相似度排序要搞清楚为什么不能只靠向量库先得明白向量检索的本质。它做的事情非常简单把文本、图片或其他内容映射成一个高维向量然后用余弦相似度或内积去算两个向量之间的距离再按距离排序返回 top-k。这个机制擅长的是语义模糊匹配——你说帮我找个靠谱的律师事务所和记忆中找过一家处理合同纠纷的律所在字面上不完全一致但向量距离很近能召回来。这是关键词搜索很难做到的。另外它天然支持多语言和同义改写这些都是它的舒适区。但你要注意相似不代表有效。向量相似度衡量的是文本在语义上接近不是这条记忆在当下语境里有价值。一条被用户推翻的旧决策语义上可能和当前问题极其相似但它就是无效的甚至是有害的。2.2 精确过滤、时间衰减、冲突更新向量库的三个硬伤我总结下来向量数据库作为记忆存储有三大硬伤。第一无法做精确过滤。像只要 2024 年 6 月之后的数据排除用户明确否定的内容只检索 userId9527 的记忆这类硬性条件向量数据库本身做不好。虽然有 metadata filter但它的底层实现是先按向量相似度召回再过滤本质上只是后置裁剪召回阶段照样会把不该出现的内容捞进来只是最后不给你看而已。过滤条件复杂的时候召回质量下降非常明显。第二没有时间衰减和遗忘机制。记忆是有时效性的。用户昨天的临时讨论今天可能就毫无价值三个月前的决策现在可能已经被推翻。向量数据库里的每一条记录都是平等的没有权重随时间和使用频率变化的概念。就算你在 metadata 里存了时间戳也只是把是否失效的判断责任推给了检索层。第三更新和删除极其别扭。传统的数据库里UPDATE 一条记录是常态向量数据库里你要更新一条记忆只能先根据 id 删除旧向量再重新计算 embedding 写入新向量。删除操作尚可但是合并两条记忆给一条记忆增加权重标记一条记忆为已否决这类细粒度更新几乎都做不到只能整条覆盖。2.3 索引开销与写入放大被低估的工程成本还有一个容易被忽视的问题索引开销。向量索引不像 BTreeB树索引那样只占少量空间HNSW 这类图索引在内存里吃得很凶。我当时往 Qdrant 里写了大概 20 万条记忆每条 1536 维向量结果内存占用直接冲到几个 GB索引构建时的 CPU 开销也不小。更要命的是写入放大每次写入一条新记忆都需要调用 embedding 模型做推理如果该条记忆还需要同步更新到倒排索引、关系图谱、结构化数据库里那一次写入就是多次开销。在对话场景下每次用户交互都可能产生 3-5 条新记忆高频写入场景下向量库很容易成为性能瓶颈。3. 把 Atkinson-Shiffrin 记忆模型搬进 Agent 架构3.1 认知科学里的三级记忆模型到底在说什么Atkinson-Shiffrin 模型也叫多重存储模型是 1968 年提出的经典记忆理论。它把人脑记忆分成三个阶段感觉记忆、短期记忆、长期记忆。感觉记忆持续不到一秒的原始感官信息绝大多数直接被丢弃。短期记忆容量有限通常认为 7±2 个组块通过复述才能保持不加工就会快速遗忘。长期记忆容量几乎无限经过编码和巩固之后形成可以持续数小时到数年。这个模型最关键的一个观点是记忆不是存进去就完事它必须经历一个从短期到长期的巩固过程而且遗忘是系统设计的一部分不是故障。后来也有很多认知科学改进比如加入工作记忆的概念、强调提取对巩固的作用但大框架至今仍是理解记忆的好起点。我当时看到这个模型的第一反应是这不就是 Agent 上下文管理的标准范式吗上下文窗口是短期记忆记忆库是长期记忆中间的遗忘机制不是 bug而是防止系统被噪音淹没的必需品。3.2 从 XMem 到 Chat Agent跨领域都在用这套分层有意思的是这个模型不仅存在于理论里。计算机视觉里的视频对象分割模型 XMem 就显式地使用了 Atkinson-Shiffrin 模型——它把视频历史分成工作记忆、长期记忆用独立的记忆网络来管理哪些过去帧的信息值得保留。这不是巧合而是所有需要长期上下文感知的系统都会遇到的共同问题信息太多必须分层重要性不同必须区分处理。我在重构 Agent Memory 的时候思路和 XMem 几乎一致短期会话里放最近的交互情境中即时使用定期把短期内容归纳、总结、提炼成长期记忆长期记忆再做进一步分级。这个模式能直接对齐认知科学和已有的视觉记忆实践说明它不是某个人的拍脑袋设计而是被验证过的通用先验。3.3 落地映射工作记忆、情景记忆、语义记忆、程序记忆各放哪参考 Atkinson-Shiffrin 和更细的长期记忆分类我把 Agent 的记忆分成四层记忆类型存储介质更新频率生命周期典型内容工作记忆短期上下文Redis / 内存极高单轮或几轮对话当前任务状态、对话历史、临时变量情景记忆episodic数据库 向量索引中数小时到数月之前做过什么任务、用户说过什么具体的话语义记忆semantic关系型数据库低长期用户偏好、项目事实、领域知识程序记忆procedural代码 / DSL 文件极低随代码Agent 的操作流程、工具使用习惯这套分层的核心原则是不同记忆的读写频率和生命周期不同不应该用同一种存储和同一种检索方式。工作记忆追求极低延迟放 Redis情景记忆需要语义召回才是向量数据库的主场语义记忆要求强一致和精确过滤应该放 PostgreSQL/SQLite程序记忆本质上是一段可执行的逻辑跟向量检索根本不沾边。我在项目里把短期工作记忆做成了会话窗口每次对话结束脚本自动把当前会话总结成三条以内的情景记忆写入长期层语义记忆则靠一个固定结构表维护只有情景记忆真正走了向量检索。这样既保留了语义召回的能力又解决了前面说的污染问题。4. 记忆的真正核心在向量之外元数据、关系与遗忘4.1 每条记忆都该有一张身份证如果要给想要重构记忆系统的朋友一个最重要的建议我会说先从设计记忆的元数据开始而不是先选向量库。一条记忆如果没有元数据它就是一堆漂浮的向量数字召回了也不知道是谁的、什么时候的、还可不可信有了元数据检索和过滤才能真正生效。我设计的最小元数据集合是这样的memory_id全局唯一标识更新、删除都靠它。user_id / project_id归属信息隔离不同用户、不同项目的记忆。created_at / updated_at时间戳用于时效判断和衰减计算。access_count / last_access_at访问频率用于重要性评分。importance显式重要性分数可由 Agent 在写入时评定。status正常 / 待确认 / 已否决 / 已过期用于过滤废弃记忆。tags结构化标签辅助检索。有了这些字段之前那个云厂商选型污染问题的解决方案就变得很简单每次检索前先把 statussuperseded 或者 statususer_rejected 的记忆排除掉再按用户 ID 做硬隔离最后按时间窗口过滤。这些动作全部发生在向量检索之前属于规则硬过滤根本不该让向量库来做。4.2 用关系层解决相关但不相像的问题向量检索有个天然缺陷它只能发现文本相似的相关性发现不了逻辑相关。举一个真实例子用户说我儿子今年要高考最近在准备志愿填报。一周后用户问你觉得学人工智能有前途吗。这两句话在向量空间里距离可能很远——一个在讲高考志愿一个在问行业前景——但它们其实是同一件事用户在为孩子的专业选择搜集信息。这种逻辑相关性向量库别想靠相似度算出来它需要关系图谱来承接。我在方案里给记忆加了一个轻量的关系层把人物孩子、事件高考、兴趣人工智能抽成实体然后用边把它们连起来。当用户提出新问题时不仅做语义检索还会尝试解析问题中的实体然后沿着图里的关系找到关联记忆。实现上不需要重型图数据库一个带 JSONB 字段的关系型数据库就行比如 PostgreSQL 里存节点表和边表查询时做两跳以内的遍历。真正需要图数据库的场景很少别过度设计。4.3 遗忘策略怎么处理冲突、过期和低频记忆遗忘是记忆系统设计里最容易被忽略、也最容易出彩的部分。大脑遗忘有规律不重要、不常用的记忆优先丢失冲突信息中新信息覆盖旧信息。Agent 也应该有同样的规则。我在项目里实现了三种遗忘机制冲突消解当用户明确提出不要 A 方案改成 B 方案时系统给 A 对应的记忆标记 statususer_rejected同时给 B 生成一条新记忆并记录 rejects 指向 A 的 memory_id。这样后续检索永远看不到 A。衰减打分每条记忆有一个综合分数 重要性 × 衰减系数(created_at) × 访问频率系数。低于阈值的记忆进入归档状态不再参与常规检索减少噪音。这个衰减可以用简单的指数函数实现score importance × e^(-λ × age_in_days) × (0.5 access_count)。定期压缩每 N 条同主题的旧情景记忆自动触发一次归纳总结生成一条更高层次的语义记忆原始细碎记录降权。有人会担心遗忘会不会把关键信息丢了。我的策略是归档而不是删除——降权、归档、需要时通过显式查询恢复这和大脑的遗忘但不彻底删除是一样的。4.4 结构化事实该进数据库而不是 embedding还有一个经常被误解的地方不是所有记忆都应该向量化。用户的姓名、生日、订阅计划、所在城市、项目预算这类结构化事实用关系型数据库存起来才是正解。它们必须精确匹配、强一致、随时更新向量检索带来的语义模糊只有坏处没有好处。比如用户搬家了从北京改到上海。在 SQLite 里就是一条 UPDATE user SET cityshanghai WHERE id...秒级完成之后所有查询都是读到的上海。但如果把用户所在城市也存成向量记忆旧记忆住在北京和新记忆住在上海会同时存在向量检索时可能同时召回然后 Agent 就会说出您住在上海之前似乎也在北京居住过这种荒谬的话。我的经验是凡是能结构化的、需要精确的事实一律走传统数据库凡是开放性的、语义关联的、无法预定义 schema 的才走向量和全文检索。这个判断标准帮我省了非常多麻烦。5. 混合检索的落地实现双通道召回与上下文组装5.1 整体架构硬过滤、向量召回、关键词召回、重排确定了分层模型之后检索流程也需要重做。我现在的检索链路是四段式硬过滤 → 双通道召回 → 融合排序 → 重排组装。第一步硬过滤。拿到用户查询后先做实体识别和分析解析出 user_id、project_id、时间范围、要排除的实体比如不要已否决的方案。这一步把候选集从几百万条直接缩小到几千条。第二步双通道召回。一个通道是向量检索负责找语义相似另一个通道是倒排索引BM25或者干脆用数据库里的 LIKE/全文检索负责找关键字精确匹配。两个通道各取 top 50。第三步融合排序。用 RRFReciprocal Rank Fusion, 倒数排名融合把两个通道的结果合并成一个列表。具体算法是对每条文档在两个通道里的排名取倒数然后求加权和排名越靠前分数越高。第四步重排组装。按元数据做最后修剪过滤 status 非法的、强制排序比如重要事件靠前、时间近的靠前、按上下文窗口预算裁剪 top-k最终拼装成 prompt 里的 context。5.2 具体实现RRF 融合和元数据过滤的代码骨架这里给出一个核心的 Python 实现骨架去掉业务细节保留关键逻辑import math from typing import List, Dict def rrf_fusion(vector_results: List[str], keyword_results: List[str], k: int 60) - Dict[str, float]: vector_results / keyword_results 是两条通道返回的 memory_id 列表 按排名计算 RRF 分数返回 { memory_id: score } scores: Dict[str, float] {} for rank, mem_id in enumerate(vector_results): scores[mem_id] scores.get(mem_id, 0.0) 1.0 / (k rank 1) for rank, mem_id in enumerate(keyword_results): scores[mem_id] scores.get(mem_id, 0.0) 1.0 / (k rank 1) return scores def hard_filter(memory_records: List[Dict], user_id: str, exclude_statuses: List[str]) - List[Dict]: 硬过滤只保留当前用户的记忆剔除已否决/已过期的记录 filtered [] for rec in memory_records: if rec.get(user_id) ! user_id: continue if rec.get(status) in exclude_statuses: continue filtered.append(rec) return filtered def build_context(memory_records: List[Dict], max_tokens: int 2000) - str: 按重要性 时间倒数排序裁出不超过 token 预算的上下文块 now time.time() def score(rec): importance float(rec.get(importance, 1.0)) age now - float(rec.get(created_at, now)) access_bonus math.log(float(rec.get(access_count, 1)) 1.0) return importance / (1.0 age / 86400.0) access_bonus ranked sorted(memory_records, keyscore, reverseTrue) context_parts, used [], 0 for rec in ranked: block f[{rec.get(tags, )}] {rec.get(content, )} tokens len(block) / 3 # 粗略按字符估算 token if used tokens max_tokens: break context_parts.append(block) used tokens return \n.join(context_parts)这个实现虽然简化了不少但已经把最重要的事情体现出来了向量和关键词是并列通道元数据是硬性的最终的上下文按重要性做预算裁剪。实际项目中你可以在硬过滤阶段加 SQL 条件直接下推到数据库让过滤发生在召回之前而不是之后。5.3 Memory Bank 的工作流Session 写入、归纳、沉淀热词里提到的 Sessions Memory Bank 工作流我很喜欢它和我的设计思路完全一致。我落地的流程是这样的Session 收集Agent 在对话中先把所有信息放在短期工作记忆Redis里不做任何持久化保证响应速度。Session 结束触发固化聊天结束或达到一定轮数后后台任务把 Session 里的内容做摘要归纳形成 1-5 条高信息密度的候选记忆。与已有记忆合并候选记忆先和长期层搜索比对如果发现同主题、可合并的旧记忆就做合并如果发现冲突走用户确认流程或者按规则判赢。写入存储分层写入。结构化事实进 SQLite情景性描述 embedding 后写向量库关系信息更新关系表。异步巩固低优先级任务定期执行遗忘策略、压缩归纳、数据清理。这个工作流的关键点是长期记忆不是实时写入的而是有延迟的固化。这个延迟是特性不是 bug——它给了系统一个缓冲期避免把用户的每句闲聊都当成长时记忆存起来。5.4 实测效果对比纯向量 vs 混合方案我在同样的数据集上用两套方案跑了评估。数据集大概是 500 次真实对话产生的 2 万条记忆测试时用 100 个真实问题进行检索人工判断召回内容是否有用方案精确率召回的多少是有用的用户否定记忆出现次数平均响应延迟纯向量 top1028%15180ms向量 元数据过滤55%3185ms向量 BM25 RRF 元数据72%1230ms完整混合 重排81%0260ms可以看到单靠向量召回精准率不到三成加上元数据硬过滤后直接翻倍混合检索再往上提升最后的重排把精确率拉到 80% 以上并且彻底消灭了用户已否定但还出现的记忆。代价只是延迟多了 80ms在绝大多数对话场景里完全可以接受。6. 工程落地中那些绕不开的坑6.1 第三方记忆框架的内存崩溃0xc0000005 排查始末有一段时间我想偷懒直接用现成的记忆框架试过一个叫 ekko-memory 的开源项目。装好之后一跑训练流程进程直接崩掉退出提示是process exited with code 3221225477 / 0xc0000005 (memory access violation)。这个错误码本质是 Windows 下的访问违例通常是程序访问了非法的内存地址常见于 C/C 扩展的数组越界、空指针解引用或者 ABI 不匹配。排查过程很痛苦第一步确认不是自己的代码问题。写了一个最小复现脚本只调用框架的 API照样崩说明问题在依赖。第二步查看崩溃模块。用调试器抓 crash dump定位到某个 .pyd 文件。第三步发现这个 .pyd 是用特定版本的编译器构建的和当前 Python 环境的 ABI 不兼容一旦调用就会写坏内存。第四步找框架维护者确认发现他们只测了 Linux 和特定 Python 小版本Windows 支持完全是实验性的。这个坑给我的教训是引用现成的记忆框架之前先看它的平台兼容性、维护活跃度以及核心 C/C 扩展是否和你的运行环境构建一致。不然你会在排错上花的时间比自己写一个还多。后来我干脆基于成熟组件自己搭控制在几百行代码反而更可控。6.2 Python 读大记忆文件被 MemoryError流式加载与内存预算做记忆系统必然要处理把历史记忆加载进上下文的场景。我一开始偷懒用json.load(open(memory.json))一次把全部记忆读进内存结果在记忆文件超过 1GB 后经常报MemoryError最夸张的一次是 OOM 直接把进程杀掉了。排查后发现两个问题问题一整个文件被一次性塞进内存JSON 解析还会产生 2-3 倍的内存膨胀纯属浪费。问题二memory 目录下还有 embedding 模型、向量索引系统总内存本来就吃紧叠加后直接爆。解决方案也很简单把记忆文件改成按 Session 分片存储每个 Session 一个文件加载时用流式 JSON 解析或者只读取当前需要的 Session再给记忆加载设置一个内存预算超过预算就用 LRU最近最少使用策略从内存里淘汰旧 Session只在检索时按需读盘。永远不要让加载全部记忆成为默认行为。6.3 Java 侧 OOM用 MAT 定位堆内堆外泄漏另一个同事负责给 Agent 做企业版 Java 服务端遇到了典型的OutOfMemoryError: insufficient memory当时以为加 -Xmx 就能解决结果加到 8G 还是挂。最后用 Eclipse Memory AnalyzerMAT做 heap dump 分析才定位到问题。MAT 分析的关键步骤启动参数加-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/path/to/dump让 JVM 在 OOM 时自动导出 heap dump。用 MAT 打开 dump 文件先看Overview - Biggest Objects by Retained Size找出占用最大的对象。点开看 Reference Chain引用链定位到是哪个业务对象把它拽在内存里。我们最终发现问题不在堆内——堆里有个缓存占了大头但真正致命的是堆外内存。向量索引的 native 内存、线程栈、直接缓冲区DirectByteBuffer都不受 -Xmx 控制。解决方式是隔离部署把向量索引的 native 内存部分拆成独立进程通过 gRPC 访问这样 Java 进程的堆外内存压力骤减。记住OOM 不一定是堆的问题先看 MAT 再决定调哪里。6.4 向量存储选型Qdrant、Milvus、Redis、FAISS 到底怎么挑前面讲的都是不能只靠向量库但不是说向量检索完全不需要。关键是怎么在合适的场景选合适的向量存储。我做了一个选型决策表直接给结论方案适合场景优点痛点FAISS本地单机、小规模、低延迟部署简单、全内存、性能最强无持久化、无元数据过滤、需要自研管理Qdrant中等规模、需要元数据过滤自带过滤、持久化、REST/gRPC 接口、部署方便索引内存占用不小配置需调优Milvus大规模、分布式、高可用扩展性好、功能全、生态完善组件多、部署重、运维成本高Redis 向量模块已有 Redis、需要毫秒级响应无额外组件、延迟极低、融合现有缓存架构向量规模受限、过滤能力弱、数据量大后内存贵关系型库的 pgvector已有 PostgreSQL、中小规模统一存储、SQL 过滤强、事务一致向量检索性能一般不适合超大规模我的个人建议中小团队、单机部署、数据量在百万级以内直接上 Qdrant 或者 pgvector数据量大到需要分布式再考虑 Milvus如果主要是短期记忆和会话缓存Redis 就够了。千万别一上来就上最重的分布式方案很多项目的记忆数据量其实连百万都不到分布式完全是给自己找运维麻烦。6.5 记忆系统可观测性别等用户发现它忘了最后一条经验可能很多人想不到记忆系统最需要的不是性能优化而是可观测性。Agent 是不知道自己忘了什么的但用户会知道。如果记忆系统召回质量出了问题用户只会觉得这个 AI 怎么这么蠢明明说过的事不记得而你作为开发者却毫无线索。所以我在系统里加了一套记忆审计日志每次检索都记录用户查询原文。检索时用了哪些过滤条件。向量通道和关键词通道各召回了什么。最终拼接进 prompt 的上下文是什么。这套日志的价值非常大。出问题的时候你可以一条条回放某个用户的所有检索过程看到他说的某句话为什么没被召回是硬过滤把它滤掉了还是向量距离太远还是重排时被挤掉了。定位一次失忆问题的速度从小时级降到了分钟级。另外我还会定期对记忆做一致性校验比如找出 status 已经变成 user_rejected 但还被反复检索的高频记忆说明过滤条件没有生效再比如统计每个用户的记忆总量和召回率异常波动往往意味着 embedding 模型或者向量数据出了问题。最后再分享一个小技巧如果你正在设计自己的 Agent Memory我强烈建议先把记忆应该分层、过滤、遗忘这些架构问题想清楚再决定用什么存储组件。向量数据库是好工具但它只是记忆系统里的一块拼图。你真正想要的不是一个能搜到相似文本的引擎而是一个能在正确的时间、以正确的粒度、把对用户真正有用的信息递到 Agent 面前的系统。我现在的架构里向量库承担的检索请求已经降到了总量的四成但整体效果反而好了非常多。这中间的差别就是元数据、关系层、遗忘策略和混合检索一起补上的。按这个思路做虽然初期多写几百行代码但后面省下来的排错时间会远远超过你当时的投入。
返回列表