Designing a Persistent Knowledge Layer That Refuses to Guess
TL;DR · AI 摘要
RAG架构无法积累知识,本文提出持久知识层设计,结合GraphRAG与Azure服务实现,解决重复推理问题。
核心要点
- RAG系统每次推理均独立,无法积累领域理解
- Azure方案整合Microsoft Foundry与Cosmos DB构建知识图谱
- GraphRAG通过实体关系网络提升推理连贯性
结构提纲
按章节快速跳转。
揭示RAG系统无法积累知识的根本问题
分析基于嵌入的缓存机制仅存储答案而非理解
提出知识图谱与推理链持久化存储方案
展示Microsoft Foundry与Cosmos DB集成架构
通过实体关系网络提升推理连贯性
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- 持久知识层架构
- RAG局限性
- 语义缓存缺陷
- 重复推理问题
- 核心设计
- 知识图谱存储
- 推理链持久化
- Azure实现
- Microsoft Foundry
- Cosmos DB集成
金句 / Highlights
值得收藏与分享的关键句。
RAG系统每次推理均独立,无法积累领域理解,导致重复计算浪费资源
Azure方案通过Cosmos DB存储推理链,实现知识持久化积累
GraphRAG将实体关系网络纳入知识图谱,提升跨文档推理能力
设计一个拒绝猜测的持久知识层 | Towards Data Science
人工智能
设计一个拒绝猜测的持久知识层
RAG进行检索,但从不记忆。一个适用于各类应用的通用蓝图,这些应用能够积累知识。包含完整的Azure原生实现(Microsoft Foundry、Azure AI Search、Cosmos DB、FastAPI),并映射到财产保险语料库。
Miodrag Cekikj
2026年8月16日
49分钟阅读
分享
照片由Will Grobbelaar在Unsplash上提供
在我的RAG-ING Ahead系列文章中,我构建了一个完整的云原生检索系统:语音和文档处理、分块、嵌入、Azure AI Search,以及一个位于其上的助手层。该系列回答了我当时的问题,即“如何让语言模型回答那些它从未接受过训练的文档中的问题”?
该系统仍然有效。检索增强生成(RAG)仍然是在不重新训练任何内容的情况下,将模型扎根于私有、特定领域或最近更改信息的最实用方法。[1] 如果你拥有一个语料库并需要从中获取答案,RAG仍然是你的起点。
但我在多个长期项目(而非演示)中运行了这一模式,逐渐产生了一个新的问题:
系统检索到相同的段落,对其进行推理并生成良好答案——然后却将所有推理过程丢弃。明天,有人提出一个相关问题时,系统会重复完全相同的处理流程,付出同样的成本,且无法保证得出相同结论。
我的第一反应是调整系统而非质疑其设计。我尝试过基于嵌入的缓存机制:识别出新问题在语义上与系统已回答过的问题相似,直接返回之前的响应而非再次执行完整检索和生成流程。语义缓存确实能有效降低成本和延迟,我仍然推荐使用。但直到很久之后我才意识到其本质——它缓存的是答案,而非理解。缓存的响应与原始响应一样毫无价值。系统对领域的理解模型并未得到任何改善,一旦问题超出相似度阈值,所有工作又必须从零开始。无论我如何调整,底层的RAG架构概念始终未变。
图1 – 我实验的语义缓存机制。命中时可跳过整个流程;未命中则从零开始。无论哪种情况,系统都不会积累任何信息——虚线框所示部分正是缺失的关键环节。图片由作者提供。
这不是一个检索问题。检索系统正在执行其设计初衷。这是一个架构问题。标准RAG系统中没有让理解能力积累的空间。无论采用多少缓存、重排序或分块策略,都无法解决这一问题,因为它们都只优化了查询效率,却未赋予系统任何记忆能力。
本文将探讨如何构建这个缺失的部分。这是我在RAG、GraphRAG以及文档语料库上的代理推理研究中的最新成果,是当渐进式调整不再奏效、必须彻底重构设计时的突破性方案。我将通过三个部分进行阐述。
第一部分与供应商无关。它将架构描述为一种设计模式:各层、对象模型、架构旨在应对的故障模式以及其所需的治理方式。这些内容不依赖于 Azure,也不依赖于任何特定的数据库或模型提供商。如果您使用的是 AWS、GCP,或者在本地运行 Postgres 配合 pgvector 和本地模型,该设计依然适用,我希望它对您也有帮助。
第二部分是 Azure 的具体实现。逐个服务进行说明,包含每个选择的依据、真实的基础设施即代码(Infrastructure-as-Code)配置,以及一个可以克隆并部署的正在运行的 FastAPI 应用程序。
第三部分是演示环节。一个名为 Ostermere Mutual 的合成财险公司、21 个相互关联的文档,以及三个演示流程,展示该模式能够实现传统检索系统无法完成的操作。
数据集中的所有内容均为合成数据。Ostermere Mutual 并不存在,监管机构、保单、索赔、人员、风区或相关数据也均不存在。此处没有任何与保险、法律、核保或索赔建议相关的内容,演示中的每一页均不代表任何真实保单的实际解释。
命名说明:当前微软文档中,将之前称为 Azure AI Foundry 的统一平台命名为 Microsoft Foundry。本文中我将统一使用 Foundry 一词,同时在有助于架构理解的场景中保留 Azure 服务名称。
目录
第一部分 – 设计:
- 经典 RAG 的适用场景与局限性
- 检索不等于知识积累
- 三层架构
- 知识层中实际存储的内容
- 六种故障模式
- 知识写入属于不同的风险类别
- 查询路由
- 不应构建此架构的场景
第二部分 – 在 Azure 上的实现:
- 层与服务的映射
- Blob 存储
- 文档智能
- Azure AI 搜索
- Cosmos DB
- Microsoft Foundry
- 容器应用上的 FastAPI
- 身份验证
- 数据摄入生命周期
第三部分 – 演示:
- Ostermere Mutual
- 范围规则
- 矛盾场景
- 损失日期
- Obsidian 保险库
- 成本论证
- 治理机制
- 我下一步将构建的内容
- 总结
完整项目(包括应用程序、基础设施、数据集和可直接打开的 Obsidian 保险库)可在配套的 GitHub 仓库中找到,地址为 github.com/mcekikj/persistent-knowledge-layer,采用 MIT 许可证。
第一部分 – 设计
1. 经典 RAG 的适用场景与局限性
传统的 RAG 流程简单到可以用一条线表示。
图 2 – 经典 RAG 管道。每个问题都从左侧开始。没有任何内容能通过右侧。图片由作者提供。
文档被分割、嵌入并存储在支持向量的索引中。一个问题到达后,系统会找到语义或词汇上相似的片段,并将其作为上下文传递给模型。模型本身对您的业务一无所知,但在此过程中会被临时“伪装”成了解业务的样子。
这解决了真实且重要的问题,我并不想低估其价值。组织的私有知识无需存储在模型的参数中,而是在需要时进行检索。这是一个真正的好主意,这也是该模式迅速传播的原因。
但请观察架构优化的目标。它优化的是查询时的检索操作。每个问题都被视为人类历史上第一个被提出的问题。
现在设想一个真实用户,在数周内处理一个真实问题时:
- 什么是实际现金价值?
- 它与重置成本价值有何不同?
- 何时可以支付可恢复的折旧?
- 哪个早期决策定义了我们如何处理折旧?
- 哪份文件引入了例外情况,原因是什么?
- 有同事告诉我阈值是15年,这是真的吗?
一个简单的RAG应用会独立回答这些问题。它会再次检索片段,重建上下文,并要求模型再次推理。每个答案可能都很好。但这种综合是临时的,这意味着当响应被交付时,理解会消失。关于第六个问题,系统已经回答了前五个,这并不会让问题更容易回答。
至于最后一个关于阈值是否是15年的问题,检索系统的表现会比失败更糟糕。它会找到提到15年的片段,并自信地告诉你“是的”。
2. 检索不是累积的理解
我反复使用的类比是一个拥有文件柜的研究员。
你提出一个问题。研究员走向文件柜,取出四份文件,阅读相关段落,然后给你一个经过深思熟虑的回答。这确实很有用。然后他们把文件放回去,扔掉笔记,忘记整个过程。明天你问后续问题时,他们又会从文件柜开始。
一个更优秀的研究员在阅读时会做其他事情。他们:
- 保存重要来源的摘要;
- 为每个重复出现的概念维护一页笔记,并连接那些被证明相关的概念;
- 记录决策及其背后的推理;
- 当新证据改变时更新比较;
- 记录矛盾而非悄悄解决;
- 保持一份仍在无法回答的问题列表;
- 并为每个主张保留指向原始文档的链接。
第一个研究员对应的是查询时的RAG系统。第二个研究员则是我想构建的系统。
这接近于Andrej Karpathy提出的LLM Wiki模式:原始资料保留在原处,而代理维护一套Markdown页面(实体、概念、比较、交叉引用),供人类阅读和导航。Obsidian恰好是查看结果的一个便捷窗口。[2]
很容易被这里的Markdown格式分散注意力,因此我需要明确说明真正重要的想法到底是什么。
重要的想法不是Obsidian。甚至也不是Markdown。重要的是知识被编译成一次性的持久化产物,而不是每次请求时都从原始片段中重新构建。
这是一个架构层面的主张,并且会产生架构层面的影响。
3. 三层架构
我不想取代RAG。我想为它提供一个存储所学内容的地方。
当需要精确措辞、引语、条款引用、新上传的文档或验证有争议的内容时,原始来源始终是最有力的依据。然而,即使精心维护的生成页面也只是衍生的解释。它不是证据,而一旦让它假装成证据,你就构建了危险的东西。
因此,设计包含三层架构,它们回答三个不同的问题。
图3 – 混合知识架构。两层都源自相同来源;彼此之间不互相替代。图片由作者提供。
层级
它回答的问题
优化目标
证据
当前有哪些相关来源材料?
召回、精确措辞、引用、新鲜度
知识
/
这个系统已经解决了什么,它目前又相信什么?
连续性、关系、综合、复用
协调器
我需要哪些要素才能安全地回答这个问题?
路由、风险、时间范围
表1 – 三层结构及其各自存在的问题。它们互不替代;各自针对不同的需求进行了优化。表格由作者制作。
这一区分是实用性的,而非哲学性的。证据层是一个检索索引。知识层是领域的一个受维护、结构化、可读性强的模型。协调器是知道关于具体政策措辞的问题应指向第一层,而关于为什么做出这个决定的问题应指向第二层的组件。
有一个规则将整个系统串联在一起,值得单独列出:
知识页面永远不能作为来源。它始终可以追溯到一个来源。
打破这条规则后,你就不再拥有知识库。取而代之的是,你拥有一堆无人能验证的自信主张。
4. 知识层中实际存在的内容
"Wiki"这个词让人联想到一个Markdown文件的文件夹。对于个人知识库来说,这确实很合适。但对于应用程序,我希望在底层使用结构化存储,将Markdown作为从中生成的视图。
我确定的对象模型如下:
图4 – 知识对象模型。每个派生对象都指向支持它的来源。图片由作者制作。
其中大部分内容并不令人意外。其中三个对象是整个模型的核心,我需要重点说明它们。
决策 – 因为"为什么"会最先消失
决策对象包含规则、适用范围、生效日期、负责所有者及其理由,并指向理由的来源。
最后一个字段的重要性可能被低估。在我的合成语料库中,一项承保更新指出,超过15年的屋顶检查会在一个特定的风区触发。该更新没有说明为什么是15。推理只存在于一个地方:分析师与承保主管之间的一封电子邮件,其中解释称,在15至20年之间,强风带的屋顶位移量约为标准带的3.4倍,并明确警告人们在没有限定条件的情况下会直接使用这个数字并将其应用于整个书籍。
电子邮件不是政策文件。任何检索系统都不会将其排名很高。在18个月后,当有人问"为什么是15?"时,这个推理就会消失——除非有人刻意保存了它。
矛盾 – 一个首要级对象,而非错误
这是我最希望你从本文中带走的核心理念,无论你从文章中使用了其他什么内容。
真实的语料库会自我矛盾。两个团队各自撰写文档,两个文档都有效且互不替代,但它们存在分歧。在任何由多个团队维护超过一年的文档集中,这种状态是常态,而非边缘情况。
检索系统对此的处理方式极其糟糕。它会检索其中一块内容,或另一块,或两者都检索,然后要求语言模型在单次前向传递中根据系统提示(要求其"有帮助")将它们统一。模型会生成一个流畅且自信的答案,但它会悄无声息地选择了一方。
更糟糕的是,它最自然的启发式方法是“较新的文档优先”。这听起来合理,但实际上是错误的。新颖性并不等同于适用性。较新的文档可能适用范围更窄,可能针对的是不同产品,或者可能由对问题没有管辖权的团队编写。
因此,这种矛盾会拥有自己的对象,包含两个声明的原文、生效日期、负责的负责人以及一个显式字段:why_not_resolved。系统的工作职责是检测到冲突后拒绝解决。
开放性问题 – 了解你所不知道的
自然的伴侣。有些问题目前还无法回答,通常是因为它们被矛盾所阻塞。一个可见的开放性问题很安全。而同样的问题如果被悄悄错误地回答,最终会出现在投诉文件中。
5. 该架构旨在克服的六种失败模式
任何架构的诚实测试是:它能做哪些简单方法无法做到的事?
我特意构建了演示语料库来回答这个问题。它包含六个不同的陷阱。一个纯粹的检索系统会落入每一个陷阱——并且会流畅地落入,生成一个读起来完全通顺的答案。
5.1 作用域覆盖
一般规则规定检查使用超过20年的屋顶。后续更新规定检查超过15年的屋顶,但仅限于一个风区,且仅适用于新业务。
检索会返回15年期限的部分。模型声称阈值是15年。现在它要求对数以万计的普通屋顶进行检查,而经纪人投诉完全有道理。
知识层存储了一个带有作用域的决策:“仅限新业务。仅限H3区。”该数字始终与其限定词一起出现。
5.2 真正的矛盾
《理赔处理手册》规定追踪和访问成本作为标准覆盖至5000欧元,处理人员可以无需转介直接授权。《附加条款目录》规定追踪和访问是一项可选付费附加条款,除非在条款表中列出,否则不予支付。
两份文件都是最新的。彼此之间没有取代关系。不同团队编写了它们。
RAG系统会选择其中一份。知识层会提出矛盾,指定负责人,并说明没有可用答案。
5.3 术语漂移
在语料库中,相同概念以实际现金价值、ACV、现金结算基础和折旧价值等形式出现。数据集中的经纪人邮件实际上列出了六个此类术语,并询问它们是六个不同的概念还是一回事。
没有实体解析时,你的维基会生成四个相互矛盾的独立页面。有了实体解析,就会生成一个页面、四个别名,对任意一个别名的查询都会导向正确位置。
5.4 生效日期作用域
一笔理赔的损失日期是2026年2月20日。一项规则于2026年3月1日生效。该规则无法适用于该理赔。
这是我最喜欢的一个案例,因为检索系统对此完全无能为力。语义相似性不包含时间信息。关于15年阈值的段落对理赔中屋顶年龄的提问具有最大相关性——但完全不适用。系统不仅错误,而且以最具有说服力的方式错误。
解决方法需要协调器知道问题涉及日期,并选择该日期生效的文档,包括保留当时处于生效状态的被取代文档。
5.5 理由丢失
Covered above. The reasoning lives in an email, the rule lives in a guideline, while the connection between them lives nowhere.
5.6 多跳
“为什么这个声明被归类为 Level 1?”需要声明注释、归类指南、水损概念以及政策条款。四次跳转。Top-k 相似性搜索不会遍历,而是进行排序。类型化关系会进行遍历。
将这些内容综合起来,构成了论点——不是因为维基更友好,而是存在一类问题能够自信但错误地检索答案,而知识层能够捕捉到这一点。
6. 编写知识与回答属于不同的风险类别
这是让我最长时间才能内化的一个观点,它彻底改变了我对整个设计的思考方式。
一个错误的聊天回答只影响一次对话。一个错误的权威概念页面会影响所有后续基于它的答案,只要它保持错误且无人察觉,因为它看起来像知识。
一旦系统开始编写持久化知识,它就从“检索应用”跨越到了“系统记录”,并需要承担相应的控制责任。
一切皆为补丁
模型从不直接写入存储,而是提出一个补丁。应用程序验证该补丁,若变更具有重大影响,则需要人工批准。
图5 – 补丁生命周期。模型提出,应用程序处理。作者提供。
注意“解决矛盾”所处的位置:从不自动处理。如果系统能自行解决矛盾,矛盾对象的存在就毫无意义。
来源是一条链,而非一个字段
图6 – 如果这条链中的任何一环缺失,该页面只是一个精修笔记,而非知识对象。作者提供。
无法识别来源的对象应被删除,而非修正。当你不知道其来源时,无法修复任何东西。
过时性是必须追踪的属性
由于底层来源被替代,某页面在周一可能是正确的,周五却可能错误。因此,每个派生对象都携带 last_validated_at 属性,其来源对象则携带 superseded_by 属性。当来源被替代时,所有派生对象将被标记为过时,直到重新推导完成前,不得作为当前内容展示。
7. 路由:哪一层来回答这个问题?
并非所有问题都需要两层处理,而将所有内容同时发送给两层会构建出昂贵且缓慢的系统。
图7 – 查询路由。矛盾检查是闸门,而非脚注。作者提供。
关于这张图,有两个刻意设计的要点。
首先,时间检查应位于检索之前,而非之后。如果你先按相似性排序,再剔除不适用的结果,不适用的文档已经消耗了你的 Top-k。在生产环境中,应将有效日期和替代日期写入索引,并在查询中直接过滤,从而在排序前限制候选集。演示在此处采取了捷径,我需要承认:它在检索后立即应用日期过滤,这在当前语料规模下行为相同,但在大规模语料中会悄悄导致 Top-k 被饿死。但原则依然成立:演示为此选择了更简单的索引架构。
其次,矛盾检查是输出路径上的闸门。无论问题经过哪条路径,只要主题存在争议,系统就会停止。用代码表示,大致如下:
def query(self, question, requested_mode, top_k, as_of=None):
mode = self.choose_mode(question, requested_mode)
wiki_items = self._search_wiki(question, top_k) if mode in {"wiki", "hybrid"} else []
evidence = self.evidence.search(question, top_k) if mode in {"evidence", "hybrid"} else []
if as_of:
# 问题所涉及的日期 - 不是提问的日期。
evidence = self._filter_by_date(evidence, as_of)
# 获取所有涉及检索概念的矛盾信息,即使该矛盾对象本身未被排序。询问追踪和访问的用户会得到冲突信息,无论他们是否使用了“矛盾”一词。
contradictions = self._contradictions_for(wiki_items)
warnings = self._warnings(evidence, contradictions, as_of)
context = self._build_context(wiki_items, evidence, contradictions, as_of)
answer = self.model.answer(question, context)
...发送给模型的指令是明确的:
未解决的矛盾。您必须同时呈现两种立场及其来源,并说明立场尚未解决。您不得在两者之间做出选择,也不得偏向较新的文档——新旧程度与适用性无关。
8. 何时不应构建此系统
我宁愿您跳过这种架构,也不愿误用它,因此让我明确说明简单RAG更优的工程场景。
在以下情况下继续使用简单RAG:
- 语料库较小且查询频率低,没有需要分摊的开销;
- 用户几乎都希望进行精确的来源检索,而非综合生成;
- 文档更新速度极快,任何衍生的综合内容在使用前就会过时;
- 会话间没有值得保留的跨会话知识;
- 是生命周期较短的原型;
- 吞吐延迟必须最小化;
- 您的组织尚无法治理AI生成的持久知识。这一点不是技术限制,但常被忽视。
在以下情况下构建知识层:
- 同一领域被反复查询,且用户的工作跨会话持续进行;
- 跨来源的综合生成是常态,而非例外;
- 决策及其理由必须在人员变动后仍能保留;
- 异常情况、适用范围和矛盾确实具有实际影响;
- 领域专家需要查看并纠正系统所持有的信念;
- 从答案到来源的审计追踪是必要要求,而非附加功能。
这是一个工作负载决策,而非新旧技术的简单对比。对许多应用来说,诚实的回答是:您并不需要这个系统。
第二部分 – 在Azure上实现
以上所有内容都经过精心设计以确保可移植性。现在让我在Azure平台上正确构建它,这是我日常使用的平台。
9. 将各层映射到服务
图8 – Azure原生架构。颜色与图3对应:红色表示静态证据,蓝色表示检索层,绿色表示知识层。图片由作者提供。
职责 | Azure服务 ---|--- 不可变的原始来源 | Azure Blob Storage 扫描的PDF、表格、表单、布局 | Azure AI Document Intelligence 分块、关键词、向量和混合检索 | Azure AI Search 聊天和嵌入模型部署 | Microsoft Foundry 结构化维基状态 | Azure Cosmos DB for NoSQL API和编排 | Azure Container Apps上的FastAPI 事件驱动的摄入 | Event Grid → Container Apps Jobs 身份和密钥管理 | Entra ID、托管身份、Key Vault 遥测 | Application Insights 知识的人工检查 | 人工检查知识
Obsidian,过度导出的 Markdown
表 2 – 每个职责映射到承载它的 Azure 服务。此处没有奇特之处——设计的关键在于组件如何连接,而非组件本身。表格由作者制作。
图 9 – Azure 门户资源可视化器中部署的资源组:Container App 及其环境、Foundry、Cosmos DB、Application Insights、Key Vault、托管身份、AI 搜索和存储账户。截图由作者提供。
10. Blob 存储 – 无法重新生成的唯一事物
此架构中的其他所有内容都可以推导出来。块可以重新分块,嵌入可以重新生成,原则上整个维基可以从头重新编译。但原始文档无法从任何东西中恢复。
因此它们得到相应的处理:
raw-sources/
{workspace-id}/
{document-id}/
original-file.pdf ← 永远不会被重写
extracted-content.json ← 文档智能输出
ingestion-metadata.json ← 运行了什么、何时运行、使用了哪个模型版本
wiki-export/
{workspace-id}/
Home.md
Concepts/ · Decisions/ · Contradictions/ · Sources/ 在 Bicep 中,关键部分是三个属性:
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-05-01' = {
parent: storage
name: 'default'
properties: {
// 原始文件必须能够经受住摄入错误。版本控制和软删除是
// 系统无法重新生成的唯一工件上最便宜的保险。
isVersioningEnabled: true
deleteRetentionPolicy: { enabled: true, days: 30 }
containerDeleteRetentionPolicy: { enabled: true, days: 30 }
}
} 在存储账户本身上,我认为在任何生产部署中都应该包含的一行:
allowSharedKeyAccess: false // 永远不要使用连接字符串维基写作过程绝不能接触原始文件。这是出处、审计和删除工作流程的要求,通过使用独立容器和狭窄的角色分配来强制执行,比依靠良好意图要容易得多。
11. 文档智能 – 选择性使用
我的演示语料库是 .txt 和 .md,因此应用程序直接读取它们。真实的保险文件是扫描件、表格、签名,其布局本身通常是关键结构。将承保限额表扁平化为段落比毫无用处更糟糕。
Azure AI 文档智能为您提供预构建和自定义模型,可返回文本、表格、选择标记和结构。[3] 我的经验法则:
- 清晰文本和 Markdown → 简单解析器,无需费用;
- 可机读 PDF → 如果确实足够,使用 PDF 解析器;
- 扫描件、表格、复杂布局 → 文档智能;
- 始终将提取的 JSON 与原始文件并存,并保留页面和跨度偏移量,以便引用可以指向具体位置,而不仅仅是文档。
最后一点在合规审查员第一次问“它到底在什么地方提到?”时就会自我证明。
12. Azure AI 搜索 – 证据层
每个索引记录都包含可搜索文本及其向量:
{
"id": "INS-SYN-004-0",
"workspace_id": "ostermere-insurance-demo",
"source_id": "INS-SYN-004",
"title": "High-Wind Zone Underwriting Update",
"content": "For new Hearthmere business in zone H3...",
"chunk_number": 0,
"content_vector": [0.012, -0.008, 0.031, "..."]
}Azure AI Search 在设计上特别适合这一点:文本字段和向量字段可以共存于一个索引中,混合查询能够并行执行全文检索和向量检索,并通过倒数排名融合(Reciprocal Rank Fusion)合并排序结果。随后语义排序可以重新排列顶部结果。[4]
这一点的重要性可能比听起来更显著。在保险领域,一半的查询是概念性问题(“什么情况属于突然的水损”),另一半是词汇性查询(“HS-TA-01 覆盖哪些内容”)。向量搜索在处理前者表现良好,但在处理后者时不可靠:嵌入向量在处理精确标识符、条款编号和产品代码时已知效果较差。BM25 在处理这些方面表现优异,但无法处理同义表达。你希望同时拥有两者,并希望将它们融合而不是选择其一。
from azure.search.documents.models import VectorizedQuery
vector_query = VectorizedQuery(
vector=self.model.embed(query),
k_nearest_neighbors=max(top_k, 10),
fields="content_vector",
)
results = self.client.search(
search_text=query, # BM25 leg
vector_queries=[vector_query], # vector leg
filter=f"workspace_id eq '{self.workspace_id}'", # security trim
select=["id", "source_id", "title", "content", "chunk_number"],
top=top_k,
)注意这里的过滤条件,因为它执行的是安全控制,而非相关性调优。
⚠️ 向量相似性不是授权机制。余弦距离的任何特性都不会尊重你的权限模型。安全过滤必须是对索引字段的硬性过滤,需在查询时对每个查询强制应用。它必须以完全相同的方式应用于知识层和导出的 Markdown。即使维基页面综合了用户无法阅读的三个文档,这仍然是数据泄露,只不过是以更美观的格式呈现。
对于更大规模的系统,集成向量化可以将分块和嵌入过程移至索引器流水线中。[5] 在此处我纯粹将嵌入调用保留在应用程序中,以便在文章中清晰展示流程。
从实际部署中获得的一个规模提示:演示运行在免费搜索层级上,对于21个文档的语料库,向量加混合搜索在此层级上运行良好。免费层级的限制在于服务本身缺少语义排序器和托管身份支持,这两个功能在Bicep中都是条件性配置,因此将基础版视为生产环境的最低标准,而免费版则是验证设计的完美方式,无需任何成本。
图10 – 在免费层级上使用Search Explorer查看保险证据索引:33个分块文档,763.9 KB的向量存储,混合查询通过@search.score将H3合理性邮件(INS-SYN-018)排在首位。截图由作者提供。
13. Cosmos DB – 知识层
NoSQL的Cosmos DB存储结构化维基。整个设计基于三个决策。
分区键为 /workspace_id。每个维基对象都携带类型区分符,因此概念、决策、矛盾和开放问题都存储在同一个容器中。这意味着单分区查询可以拉取整个工作区的知识,无需跨分区扩散,这正好符合该系统的访问模式。
partitionKey: {
paths: ['/workspace_id']
kind: 'Hash'
}目前没有图数据库。人们一听到“关系”就会想到Gremlin或Neo4j。我建议暂缓这种想法。文档存储中的显式关系记录已经能够处理该系统实际执行的所有操作:查找邻居、沿类型边前进、渲染带有链接的概念页面。这只需要一到两跳。
图数据库在需要深度遍历的场景中才能发挥价值:多跳影响分析、中心性计算、大规模网络中的路径查找。如果你的应用场景并非如此,那么你实际上是在为第二个数据库和第二个查询语言买单,仅仅是为了避免编写 WHERE c.source_id = @id 这类语句。
目前暂采用无服务器架构。计费基于消耗的请求数量单位,这种模式适合演示和初期波动较大的工作负载。当实际测量出真实的RU消耗后,再迁移到预置吞吐量模式,而不是提前迁移,更不能仅仅因为"无服务器"这个词流行就盲目迁移。[6]
数据平面访问使用 Cosmos 自带的 RBAC 系统(SQL 角色分配),该系统独立于 Azure RBAC,容易让人误操作:
resource cosmosDataRole 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-11-15' = {
parent: cosmos
name: guid(cosmos.id, identity.id, 'data-contributor')
properties: {
principalId: identity.properties.principalId
// 00000000-...-000000000002 是内置的 Cosmos DB 数据贡献者角色
roleDefinitionId: '${cosmos.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002'
scope: cosmos.id
}
}配合 disableLocalAuth: true 设置,不存在密钥泄露的风险。
图11 – Cosmos DB 中的 con-001 矛盾实例:状态未解决,包含两条带来源和生效日期的声明,有明确责任人,以及保持开放的原因。矛盾是一个可存储、可查询的对象,而非脚注。截图作者提供。
14. Microsoft Foundry – 先模型后代理
Foundry 提供聊天和嵌入式部署功能。应用程序通过 Azure OpenAI v1 接口与之通信,使用标准 OpenAI Python SDK,这意味着无需每几个月就追逐 api-version 参数:[7]
from openai import OpenAI
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
credential = get_bearer_token_provider(
DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default"
)
client = OpenAI(base_url="https://YOUR-RESOURCE.openai.azure.com/openai/v1/", api_key=credential)
response = client.responses.create(model="YOUR-CHAT-DEPLOYMENT", input=prompt)在本次演示中,FastAPI 应用显式地协调各个步骤:生成嵌入向量、检索、搜索维基、构建上下文、调用模型、验证、写入。我特意这样设计,因为每个步骤都可见,你可以在任意步骤设置断点。
自然的演进方向是 Foundry Agent Service,此时相同的协调流程将转化为具有受限工具集的代理:[8]
search_evidence()
search_wiki()
get_concept()
check_contradictions() ← 作为工具的关卡
propose_wiki_patch() ← 仅提出建议;无法应用
apply_approved_patch() ← 需要审批令牌
export_obsidian_vault()我不会在第一天就授予代理不受限制的数据库访问权限。每个工具都强制执行自己的验证、授权、日志记录和狭窄的输入模式。请注意 propose_wiki_patch 和 apply_approved_patch 是两个独立工具:代理可以访问第一个工具,但要在人类介入的情况下才能访问第二个工具。这种隔离构成了整个治理模型,表现为 API 接口。
对于数据提取,使用结构化输出,将响应限制为 JSON Schema 而非仅仅要求有效 JSON。[9] 这种差异在生产环境提取首次返回"几乎有效"JSON时就会变得显而易见。
我现在可以基于实际经验进行说明,因为部署这个架构产生了两份值得分享的现场笔记:
- 在第一个文档中出现截断的 JSON。gpt-5-mini 是一个推理模型,其推理令牌从与答案相同的最大输出令牌预算中支出。由于有 5,000 个令牌的限制,提取出的 JSON 在字符串中间被截断。仓库中的修复方案:将预算提升至 16,000 个令牌,推理时设置 {“effort”: “low”} 用于提取工作,使用 JSON 模式(text.format: json_object)约束解码器,并进行一次重试。一个让我花费两次失败种子运行细节的问题:第一次完整种子在未启用 JSON 模式时成功运行,随后两次运行在不同文档上失败。仅依赖提示生成的 JSON 不会稳定失败——它会间歇性失败,这更糟糕。受模式约束的结构化输出仍然是真正的解决方案;这是务实的选择。
- 部署命名的特殊之处。一个嵌入式部署与模型名称完全相同(text-embedding-3-small)在所有状态检查中显示为健康,但在 v1 路由和经典路由上的每次调用都返回 unknown_model。一个名为 embed-3-small 的相同部署在首次请求时正常工作。聊天部署不会出现此问题。Bicep 现在将部署名称和模型名称作为独立参数处理,此故事已包含在描述中。
15. 在容器应用上部署 FastAPI
API 接口:
GET /health
GET /wiki 列出对象,可通过类型过滤
GET /wiki/contradictions 矛盾注册表
GET /wiki/{item_id}
POST /query { question, mode, top_k, as_of }
POST /ingest/text
POST /ingest/file
POST /cost-estimate
POST /export/obsidian
POST /export/obsidian.zip一个耗费我调试时间的实现细节,值得分享:/wiki/contradictions 必须在 /wiki/{item_id} 之前声明,否则 FastAPI 会优先匹配路径参数,愉快地查找 id 为 "contradictions" 的维基对象。路由顺序具有重要意义。
容器应用是此处的正确运行时,因为它基于容器且无需我运行 Kubernetes。[10] 扩展配置值得特别说明:
scale: {
// 生产环境,1 个预热副本可避免冷启动;演示环境,0 个副本成本为零。
minReplicas: minReplicas
maxReplicas: 3
}minReplicas 是一个参数(默认值为 1),因为正确答案取决于你运行的内容。在生产环境中,缩放至零看起来像免费的钱,但每次缩放后首次请求会收取冷启动和模型握手的费用。对于从开发机器访问的演示环境,零是完美的选择——本文中验证的部署在空闲时运行于零副本且成本为零。
16. 身份验证 – 任何地方都不使用密钥
整个部署基于用户分配的托管身份运行,角色范围严格限定,所有服务均禁用本地认证。容器应用通过环境变量获取 AZURE_CLIENT_ID,DefaultAzureCredential 自动识别该变量,应用程序从未被颁发任何密钥。
{ name: 'AZURE_CLIENT_ID', value: identity.properties.clientId }在架构中,这比普通 RAG 应用更重要,有必要说明原因。检索系统仅读取数据。而此系统会写入持久化知识,其他答案将基于这些知识构建。凭证泄露的影响范围不再是“有人读取了你的文档”——而是“有人修改了你组织所相信的内容”。因此,请相应地对待写入路径。
az group create -n rg-wikirag-demo -l swedencentral
az deployment group create -g rg-wikirag-demo -f infra/main.bicep -p namePrefix=wikirag17. 数据摄入生命周期
图12 – 完整的数据摄入流程。注意步骤11-13:现有维基百科内容被加载到提取上下文中,这使得实体解析成为可能。图片由作者提供。
中间那个关键细节是人们容易忽略的。当模型从新文档中提取概念时,必须了解维基百科已有的知识。否则,模型会将"现金结算基础"视为全新概念,而不知道实际现金价值(ACV)已经以该别名存在,这将导致知识库悄然分叉。
演示中的实体解析实现:
def _resolve_concept(self, name, aliases):
"""精确id → 标题 → 别名。
没有这个机制时,'cash settlement basis'、'depreciated value'和'ACV'会各自成为独立页面,维基百科会碎片化为同义词集合。演示仅实现别名匹配。生产系统会在中间模糊阶段增加嵌入相似度计算和LLM仲裁步骤,这也是有趣失败案例的集中地。
"""
candidates = {name.lower(), *(a.lower() for a in aliases)}
for concept in self.repository.list_items("concept"):
if concept["id"] == slugify(name):
return concept
known = {concept["title"].lower(), *(a.lower() for a in concept.get("aliases", []))}
if candidates & known:
return concept
return None演示版本附带了一个通过测试,该测试使用"depreciated value"短语摄入文档,并断言不会创建新概念页面,而是会解析到已有的actual-cash-value。
如果没有更强的解析步骤,以下是实际运行结果而非理论推演。当我将全部21个文档通过部署的堆栈进行实时gpt-5-mini提取时,模型提出的概念只能被别名匹配部分解析。结果产生了149个概念对象,而精心构建的图谱仅有19个——碎片化系数约为7倍。机器提取的概念并非错误,只是命名方式超出了任何别名列表的预期("屋顶检查要求"、"EUR 5,000授权限额")。这个数字就是嵌入相似度和LLM仲裁在解析链中不可或缺的实证。
第三部分 – 演示
18. Ostermere Mutual
合成语料库包含21个文档,用于虚构的财产保险公司。文档数量足够小,半天即可读完,并特意设计使第5阶段提到的6种失败模式全部可见。
类别 | 文档 --- | --- 保险政策与产品 | 保险概览v1.2(已废止v1.1)、第4条条款(水损逃脱)、附加条款目录 核保 | 屋顶指南(20年)、H3更新(15年)、风区登记表、转介矩阵、验勘师小组备注 理赔 | 三阶处理指南、理赔手册第7章、三个理赔文件
(CLM-1042, CLM-1108, CLM-1155)
监管与合规
Veyland circular(虚构监管机构),公平理赔处理标准,知识溯源标准
非正式
两封电子邮件线程,工作组会议纪要,客户常见问题解答
表3 – 按类别划分的21个文档合成语料库。底部的非正式来源记录了矛盾,并且是15年门槛唯一合理性的存在位置。表格由作者制作。
这些非正式来源并非装饰性内容。会议纪要中正式记录了该矛盾未解决的状态。电子邮件线程中存在对15年门槛的唯一解释。根据我的经验,这正是真实组织运作的方式:规则写在文档中,而原因则保存在某人的收件箱里。
关于数据溯源的说明:语料库中的每个文档均为合成内容,由我(使用AI润色)为本文撰写,因此其使用不存在任何授权限制。该数据集随代码库一起发布,采用与代码相同的MIT许可证。
将该语料库编译后可生成:
21个来源 · 19个概念 · 28个带类型的关联关系 · 4个比较 · 5个决策 · 2个矛盾 · 4个开放问题 · 2个流程
19. 示例一 – 限定性规则
curl - X POST http://localhost:8000/query - H 'Content-Type: application/json' - d '{
"question": "What is the roof inspection threshold?",
"mode": "hybrid"
}'
普通检索会找到H3更新,其内容说明为15年,并直接显示“15年”。
知识层返回决策对象:
{
"id": "h3-roof-inspection-threshold",
"type": "decision",
"summary": "对于新业务在H3区域的Hearthmere业务,当主要屋顶覆盖物超过15年时,需请求屋顶检查。在H3区域外,一般超过20年的门槛仍然有效...",
"scope": "仅适用于新业务。仅适用于H3区域。不适用于在H3区域模型建立之前签发的现有保单。",
"rationale": "在15至20年之间,严重风力带中屋顶覆盖物完全或几乎完全位移的频率约为标准风力带的3.4倍...",
"rationale_source": "INS-SYN-018",
"accountable_owner": "D. Lindqvist(财产核保主管)"
}这个数字永远都会与其适用范围一起出现。而从电子邮件中恢复的合理性解释也清晰可见,这意味着在十八个月后,当有人询问为什么是15年时,答案已经存在。
顺便提及,该电子邮件的作者在写作时就预见了这种确切失败:
<em>“请不要让这变成一个普遍适用的15年规则。如果有人在没有区域限定词的情况下阅读这个更新,他们将把它应用于整个业务范围,我们将不得不对数以万计的普通屋顶要求检查,而经纪人投诉将完全有道理。”</em>
这是一段我撰写的合成引文,用于说明某个观点,但我认为这并非不现实的预测。
20. 示例二 – 矛盾
这是我向怀疑论者展示的示例。
curl - X POST http://localhost:8000/query - H 'Content-Type: application/json' - d '{
"question": "Is trace and access covered under Hearthmere?",
"mode": "wiki"
}'检索系统会给出答案。它从理赔手册或附加条款目录中检索出一段内容,并告诉你“是的,标准覆盖上限为5000欧元”或“只有在你购买了HS-TA-01时才适用”。这两个答案都由真实文档支持,但都是错误的,因为公司并未对此问题作出明确立场。
混合系统会这样回应:
{
"answer": "该问题尚未达成一致。知识库中存在未解决的矛盾,因此不提供答案...",
"warnings": [
"未解决的矛盾(con-001):追踪与访问 - 标准覆盖还是付费附加条款?系统不会在冲突来源之间做出选择。
负责人:Y. Tanaka(产品)"
],
"contradictions": [{
"id": "con-001",
"status": "未解决",
"accountable_owner": "Y. Tanaka(产品)",
"statements": [
{ "source_id": "INS-SYN-012", "locator": "第7.3章",
"effective_date": "2025-11-01",
"statement": "在Hearthmere下,追踪与访问费用标准覆盖,上限为5000欧元..." },
{ "source_id": "INS-SYN-008", "locator": "HS-TA-01",
"effective_date": "2026-01-01",
"statement": "追踪与访问是可选附加条款(HS-TA-01),...若未在保单中列出,追踪与访问费用不予赔付。" }
],
"why_not_resolved": "两份文件均有效。彼此之间没有替代关系。它们由不同团队编写。附加条款目录是较新的文件,但新旧程度不等于适用性..."
}]
}两个陈述。两个来源。两个日期。一个需要负责的人。但没有答案,因为没有人能决定。
这个JSON是演示模式下的确定性框架。这是部署后的系统对相同问题的处理方式 – 使用gpt-5-mini模型,实时运行在Cosmos DB和AI搜索之上:
该问题存在一个未解决的矛盾。请不要将该问题视为已解决。
- INS-SYN-012(第7.3章,生效日期2025-11-01):"在Hearthmere下,追踪与访问费用标准覆盖,上限为5000欧元。处理人员应在该限额内自行批准合理支出,无需上报。"
- INS-SYN-008(HS-TA-01,生效日期2026-01-01):"追踪与访问是可选附加条款(HS-TA-01),限额5000欧元,预估保费24欧元。若未在保单中列出,追踪与访问费用不予赔付。"
该矛盾尚未解决。负责人:Y. Tanaka(产品)[con-001]。
由于上述未解决的冲突,我无法判断追踪与访问是否在Hearthmere下覆盖。
模型并未被要求普遍谨慎。它被提供了矛盾对象和一个规则,并遵循了该规则。
我真正感兴趣的第二层影响是:一旦矛盾成为存储对象,其他事物可以依赖它。语料库中包含一个保单持有人关于理赔CLM-1155的问题(漏检调查费用是否可赔付?),这个问题在矛盾解决前无法回答。因此,它被存储为一个开放问题,blocked_by: "con-001"。
系统知道为何无法回答,也清楚在问题解决前需要谁来决策。
21. 第三次演练 – 损失日期
声明 CLM-1108:风暴造成的损害,屋顶覆盖物被移位,损失日期为 2026 年 2 月 20 日。屋顶已有 18 年历史。根据当前分类,该房产位于 H3 区。
如果向检索系统询问是否需要检查,系统会找到 H3 规则(15 年,且 18 > 15),并告知您屋顶已超过阈值,且没有检查记录在案。这听起来像是一个发现。但这是错误拒赔的开始。
因为 H3 规则于 2026 年 3 月 1 日生效,距离损失发生后九天。而保单是在 2025 年 9 月签发的,当时区域模型尚未存在,因此在屋顶脱落当天,该房产并未被归类到任何区域。
curl -X POST http://localhost:8000/query -H 'Content-Type: application/json' -d '{
"question": "What roof inspection threshold applied to this property?",
"mode": "evidence",
"as_of": "2026-02-20"
}'as_of 参数是问题所涉及的日期,而不是提问的日期。设置该参数后,INS-SYN-004 完全被排除在候选集之外。该规则当时并未生效。适用的阈值是通用的 20 年规则,屋顶已有 18 年历史,低于阈值,且没有违反任何要求。
这种机制在相反方向同样适用。如果以 as_of: "2025-08-01" 的方式询问结算依据,系统会检索已过时的 v1.1 版保单,因为这是在该日期发生损失时适用的版本,并标记为:
{
"source_id": "INS-SYN-009",
"in_force_at_as_of": true,
"note": "superseded today, but in force on 2025-08-01 - this is the version that
governs a loss on that date"
}在 v1.1 版中,折旧不可收回。在当前的 v1.2 版中,折旧可收回。相同条款、相同保单、相反答案,而唯一区别是相似性搜索无法识别的日期。
这两种行为在仓库的测试中均有覆盖,且在实际部署中均可复现。在 as_of: 2026-02-20 时,部署的模型回答:
简短回答:适用通用的 20 年以上检查阈值。[INS-SYN-003]
原因:在 2026-02-20,Hearthmere 指南要求当主要屋顶覆盖物超过 20 年时需进行检查 [INS-SYN-003]。H3 的 15 年阈值直到 2026 年 3 月 1 日才生效;该保单于 2025 年 9 月 14 日签发(因此不适用 H3 规则),损失时屋顶约有 18 年历史。[INS-SYN-018] [INS-SYN-013]
在 as_of: 2025-08-01 时:
- 结算依据:实际现金价值(ACV)——定义为重置成本减去折旧——适用于 Ostermere Hearthmere v1.1 版下符合条件的建筑索赔。[INS-SYN-009]
- 折旧是否可收回?否。在 v1.1 版中,修复后折旧不可收回。[INS-SYN-009]
如果必须删除所有其他测试,我会保留这两个测试。
再举一个实时且未经脚本处理的例子:多跳问题
第 5.6 节声称,类型化关系使系统能够遍历相似性搜索只能排序的区域。我之前并未为它编写演示脚本,因此我直接向部署的系统提问:“解释为什么 CLM-1042 水损在财产索赔分诊流程中被归为一级分诊。” 系统的回答完整地走过了整个链条:
- 一级分诊标准:一级——定义为“轻微可见损坏,无安全问题,无持续进水,且预估暴露金额低于 5000 欧元” [INS-SYN-002]。
- CLM-1042 符合这些标准的依据:房屋仍可居住,未报告电气故障 [INS-SYN-005];水流在约 40 分钟内停止 [INS-SYN-005];损害仅限于水流路径 [INS-SYN-005; INS-SYN-014];初步维修预估费用为 3,800 欧元,低于 5,000 欧元 [INS-SYN-005; INS-SYN-002]。
- 支持性操作指引:《理赔处理手册》列出了突然水灾的指标……这些与 CLM-1042 的事实相符 [INS-SYN-012]。CLM-1155 明确将 CLM-1042 对比为可追溯的突发故障 [INS-SYN-014]。
- 关于临时状态的说明:承保范围和除外责任仍需由授权理赔专家进行正式承保范围判定 [INS-SYN-005]。
四个来源,四次跳转,最终的人工决策边界在未被要求的情况下得以保留。
同一会议中的一条诚实观察:我最初对该问题的表述(“……水损承保如何适用?”)因矛盾而受阻——CLM-1042 的证据涉及争议性的追溯与访问概念,因此系统返回了 con-001 并拒绝做出明确判定,而非回答优先级判断部分。该限制机制倾向于阻止判断。对于理赔工作流,我认为这是合理的默认设置,但这也是一种真实权衡:过于激进的限制机制有时会阻碍用户真正需要的答案,而调整这一边界是系统运行的一部分。
22. 直观呈现:Obsidian 保险库
该应用将整个 Cosmos DB 状态导出为保险库。55 页内容,全部自动生成,没有任何手动编写:
obsidian_vault/
Home.md
Open Questions.md
Concepts/ 19 页
Sources/ 21 页
Decisions/ 5 页
Comparisons/ 4 页
Contradictions/ 2 页 ← con-001, con-002
Processes/ 2 页图 13 – 生成知识图谱的切片。红色聚类表示未解决的矛盾及其阻断的所有内容。图片由作者提供。
图 14 – 生成保险库的图谱视图:55 页内容,全部由导出器从结构化存储中生成。概念、比较、矛盾和来源构成一个连通地图,Home 作为中心枢纽。con-001 和 con-002 作为普通节点嵌入其中,而非脚注。截图由作者提供。
在 Obsidian 中打开该文件夹,可以导航链接、检查反向链接、追踪图谱、识别孤立页面,最重要的是,可以看到系统认为的内容并告知其错误。
我认为,最后这个能力是整个架构最强有力的论据。向量索引在操作上非常优秀,但对领域专家来说完全不透明。你无法将一个 1,536 维的嵌入向量交给核保人并问:“这看起来对您来说正确吗?”但你可以给他们一个 Markdown 页面,说明阈值是 15 年,仅适用于新业务,并解释原因,以及这四个来源文档。
他们会在 30 秒内告诉你是否正确。Markdown 使真正了解领域的人能够审计系统记忆。堆栈的其他部分都无法做到这一点。
23. 关于成本的诚实讨论
知识层在数据摄入阶段成本更高。我不会假装这不是事实。
RAG流水线对每个文档仅进行一次提取和嵌入。混合流水线在此基础上还增加了总结、提取概念和主张、与现有维基进行实体解析、生成关系和比较补丁、验证以及重新生成Markdown的步骤。写放大是真实存在的,而且数值并不小。
论点在于这种前期投入的成本能够减少后续重复查询时的推理开销。因此,问题的关键不在于维基构建是否成本更高(显然确实更高),而在于这种构建成本是否能通过足够多的未来使用进行分摊。
cost_model/cost_model.py中的模型:
Compilation = D × Td × M
Simple RAG = Q × Tr
Hybrid = Q × (Tw + V × Tv)以示例默认参数计算——50个文档,每个文档6,000个token,1,000个问题,每个RAG问题检索6,000个上下文token,而维基每个问题使用1,350个上下文token,对25%的问题进行原始验证:
指标
令牌数
源语料库
300,000
维基构建
501,000
简单RAG(1,000个问题)
6,000,000
混合(1,000个问题)
1,725,000
节省的上下文
4,275,000
盈亏平衡点
约117个问题
表4 – 示例默认参数的令牌量:50个文档,1,000个问题。构建成本需预先消耗501,000个令牌,但能在查询时节省4,275,000个令牌,约在117个问题时达到盈亏平衡。表格由作者制作。
图15 – 混合方案的曲线从零点上方开始:这就是构建成本。它在约117个问题时与简单RAG方案交叉。图片由作者制作。
现在需要说明一些注意事项,因为这种图表很容易产生误导:
- 这是令牌数量,而非价格。它忽略了输出令牌、嵌入成本、AI搜索容量、Cosmos RU、容器应用计算和文档智能页面等成本。
- 它将每个令牌视为等价。实际上,摄入和回答可能使用不同类别的模型,而真正的节省主要体现在这里:小型模型可以从简洁的维基上下文中回答问题,而更强的模型则保留用于协调和更新。
- 它不能保证任何结果。如果代理程序管理不当,每次摄入都重写整个维基,那么你建模的任何节省都会被抹去。经济性完全取决于纪律性的更新策略。
一个实际测量数据点:部署该架构时,通过实时gpt-5-mini提取和嵌入处理所有21个文档,运行本文所有示例流程,并从Cosmos DB重新生成知识库,总成本约为0.20-0.30美元。闲置架构——免费搜索、可扩展至零的容器应用、无服务器Cosmos——每天消耗约0.05美元。在该语料库规模下,构建成本仅相当于一杯咖啡的钱。只有在达到规模时,经济性才会变得有意义,这正是上述模型所针对的场景。
图16 – 资源组在整个验证周期内的成本分析:总成本0.16美元,其中几乎全部是gpt-5-mini令牌成本。Cosmos DB、存储和日志分析仅以美分计,免费搜索层级为零成本。截图由作者提供。
上下文大小假设在与实际部署系统的接触中也得到了验证。在对实时堆栈的一组概念性问题进行测量时,维基上下文的平均长度约为500个标记,而等效证据上下文的平均长度约为1750个标记,比例为3.5:1,与模型假设的4.4:1处于同一数量级。绝对数值比模型假设的小,因为合成文档较短。比例才是可以转移到真实语料库的关键因素。实际上,测量出的比例甚至比假设的更保守,这会将盈亏平衡点向后推移而非提前——在预算会议引用模型时这一点值得提前了解。
使用你自己的假设运行/cost-estimate。然后丢弃这些假设,改用实际语料库和查询组合的遥测数据,通过当前Azure计算器[11]进行计费。
坦白说,令牌论点是支持这种架构的最弱论据。真正的价值体现在:
- 不同会话和不同用户之间的一致术语;
- 决策和理由能够独立于原始制定者留存;
- 矛盾以可见方式呈现而非被静默解决;
- 从任何答案到原始来源的完整审计轨迹;
- 域专家可审查的知识成果;
- 代理会话和模型升级之间的连续性。
我愿意接受数百万个输入令牌。
24. 治理,重述
由于系统具有写入能力,因此需要控制权,这是只读系统所不具备的。
保留来源信息。每个陈述都必须追溯到某个片段、某个跨度、某个文档,以及其衍生时的版本。
采用补丁方式,而非直接写入。模型提出建议,确定性验证后,对于任何重要事项必须由人工最终确认。
检测过时信息。通过last_validated_at、superseded_by、source_effective_date字段实现。当源信息被替代时,所有衍生内容将变更为过时状态,直到重新推导。
在每一层都严格控制安全。包括Blob路径、搜索记录、Cosmos对象、Markdown导出、代理工具、缓存和遥测数据。必须始终如一。合成页面绝不能显示读者在原始来源中无法访问的信息。
保持人类决策的本真性。在演示中,AI可以总结申请信息、检索政策证据、识别缺失信息、提出临时处理级别并标记冲突规则。但AI不能决定覆盖范围、拒绝索赔、评估欺诈、评级风险、解决矛盾,或告知客户决策结果。
我能够写出的最清晰表述体现在合成工作组会议纪要中,我将允许它作为整个架构的治理原则:
目前处理程序在约90%的案例中会采用助理提出的处理级别,这本身是可以接受的,但前提是他们正在亲自阅读申请信息。将处理程序排除在流程之外,就消除了使这90%可信度的依据。
这就是问题的精髓所在。自动化系统在人工审核下获得可信度后,这种可信度反而被用作取消审核的依据。
- 高风险补丁的人工审批界面:目前生命周期仅存在于设计阶段,而低风险路径已实现于代码中。
- Foundry Agent Service 工具,其中 propose 和 apply 作为独立授权的接口。
- 用于检索、合成和难点任务的评估集:更新准确性。如何验证知识库是否正确更新?
- 新鲜度与矛盾监控看板。无人查看的矛盾登记表只是日志文件。
- 按租户粒度的端到端安全裁剪。
- 基于任务复杂度和风险的模型路由。
第7点是真正开放的研究问题,目前我尚未找到好的解决方案。
26. 总结
RAG 是该架构的证据引擎,这里没有任何关于其终结的论断。它为模型提供了原始、相关、最新的来源材料,这种价值无可替代。
但它无法实现记忆功能。
知识层补充了缺失的部分:一个经过维护、结构化且可检查的系统已推导出内容的表示形式,完整保留了作用域、推理过程、可见的矛盾,并为每个主张都保留了指向证据的追溯链。
RAG 的问题是:这个问题应该检索哪些内容?
知识层的问题:在处理所有信息后,哪些内容应该被持久化为事实?
协调器的问题:为了安全地回答当前问题,我需要哪些内容?
在 Azure 上,这种分离清晰对应:
- Blob Storage 保存原始内容——这是唯一无法重新生成的部分。
- Document Intelligence 提取复杂内容并保留原文位置信息。
- Azure AI Search 存储可检索且经过安全裁剪的证据。
- Cosmos DB 存储不断演进的概念、关系、决策和矛盾。
- Microsoft Foundry 提供模型及通往代理的路径。
- 容器应用上的 FastAPI 运行协调、时间范围控制和矛盾校验。
- Obsidian 让所有内容对判断其正确性的人员可见。
这比简单 RAG 更复杂,数据摄入成本也更高。作为回报,你将获得组织记忆、一致的合成、显式的关联关系、可见的决策历史,以及我反复强调的关键能力:系统会告诉你“我们的两份文档存在分歧且尚未决断”,而不是自信地编造答案。
这种能力不是功能特性,而是构建这个系统的根本原因。
应用不再只是在文档堆中搜索,而是在持续构建一个可审查的领域模型,同时保持原始证据足够接近,以便验证每个重要结论。
感谢你花时间与我一起探索这个架构。这篇文章比我预期的要长,因为设计不断涌现出更多值得解释的部分。FastAPI 项目、Bicep 模板、21 个合成文档和 Obsidian 知识库都在代码库中,我坚信它们为你构建自己的持久知识层提供了实用起点。克隆它,在无需 Azure 凭证的情况下以演示模式运行,并尝试破坏它——我真诚地想了解它的失败之处。
声明:我是微软 MVP。本文反映我自己的独立工作和观点;微软未参与或审查本文内容。所有 Azure 使用情况均基于公开文档和我的实际部署。
[1] P. Lewis 等, 面向知识密集型NLP任务的检索增强生成 (2020), NeurIPS 2020
[2] A. Karpathy, LLM Wiki (2025), GitHub Gist
[3] Microsoft, 文档智能布局模型 (2026), Microsoft Learn
[4] Microsoft, 混合搜索概述 – Azure AI 搜索 (2026), Microsoft Learn
[5] Microsoft, Azure AI 搜索中的集成向量化 (2026), Microsoft Learn
[6] Microsoft, Azure Cosmos DB 无服务器版 (2026), Microsoft Learn
[7] Microsoft, Azure OpenAI v1 API 生命周期 (2026), Microsoft Learn
[8] Microsoft, Foundry Agent 服务概述 (2026), Microsoft Learn
[9] Microsoft, 使用 Azure OpenAI 生成结构化输出 (2026), Microsoft Learn
[10] Microsoft, 在 Azure 容器应用中部署 Flask 或 FastAPI Web 应用 (2026), Microsoft Learn
[11] Microsoft, 规划和管理 Azure AI 搜索成本 (2026), Microsoft Learn
撰写者
查看 Miodrag Cekikj 的所有文章
深度解析
,
文档检索
生成式AI
知识图谱
RAG
分享本文
- 在Facebook上分享
- 在LinkedIn上分享
- 在X上分享
Towards Data Science 是一个社区出版物。提交您的见解以触达全球受众,并通过 TDS 作者支付计划获得收益。
更新为您的实际投稿URL
为 TDS 撰写文章
✦ 结束 CTA ✦