- AI
- 模型推理服务
- 推理引擎
- 本地部署
- 多模态
【免费下载链接】runanywhere-sdks
Production ready toolkit to run AI locally
导读
本文以 core/tests/data/rag_sample.md 这份位于 RunAnywhere 核心仓库测试数据目录下的示例文档为切入点,深入剖析 RunAnywhere 本地检索增强生成(RAG)能力的完整技术链路:从文档分块、向量化索引、混合检索、上下文组装到 LLM 生成,再到内容寻址去重、会话级阈值覆盖、文档作用域隔离等进阶能力。读完本文,你将掌握 RunAnywhere RAG 会话的配置参数语义、Python SDK 的实际调用方式,以及这套能力在仓库测试与端到端验证中的真实工作方式。
1. 这份测试语料是什么,为什么值得关注
rag_sample.md是一份精心构造的虚构文档,讲述了一个名为Zephyr Protocol(西风协议)的"设备端数据同步标准"。它虽然不是真实技术规范,但在 RunAnywhere 仓库中扮演着重要角色——它是 RAG 端到端测试的标准输入语料。
从文档结构看,它包含了 RAG 测试所需的全部理想要素:
- 明确的主题标题:
# The Zephyr Protocol,便于检索系统建立清晰的语义锚点; - 分节组织的内容:
Core design、Conflict resolution、Security、Adoption四个小节,覆盖 4096 字节定长帧、内容寻址去重、ADVERTISE/REQUEST/DELIVER 三阶段握手、混合逻辑时钟冲突解决、HKDF 派生密钥加密等具体技术细节; - 可回答的事实性内容:例如"同一帧永远不会被重复传输"(content-addressed design)、"Breeze 用 Rust 编写并以单一静态库发布"等,这些都能作为接地(grounded)回答的验证点;
- 可被检索命中的专属词汇:如
Zephyr、Meridian、Breeze、HKDF等,方便测试断言检索结果确实来自目标文档。
在 core/tests/test_rag_e2e.cpp 中,这段文本被读取为doc_text,作为问答与检索断言的唯一数据源;而 core/tests/CMakeLists.txt 通过编译期宏RAG_SAMPLE_DOC_PATH将该文件路径注入测试程序。理解这份语料的构造思路,也就理解了 RunAnywhere RAG 测试的设计哲学:用一个可控、自包含、信息密度高的语料,去验证整套检索生成管线的正确性。
2. RAG 管道全景:一份文档从 ingest 到 answer 的旅程
RunAnywhere 的 RAG 能力由 C++ 核心(core/src/features/rag/目录)实现,通过 proto ABI 暴露给各语言 SDK。以rag_sample.md为例,一份文档的完整旅程如下:
2.1 分块(Chunking)
文档首先被 DocumentChunker 切分为带重叠的文本块。分块配置由 ChunkerConfig 定义:
| 配置项 | 默认值 | 说明 |
|---|---|---|
chunk_size | 180(内部) / 512(proto 层默认) | 每个块的近似 token 数 |
chunk_overlap | 30(内部) / 64(proto 层默认) | 相邻块之间的重叠 token 数 |
chars_per_token | 4 | token 计数的粗略字符估算 |
分块采用递归切分 + 句子边界优先策略(rag_chunker.cpp):优先在分隔符(如空行、句号、空格)处断开,避免从句子中间截断;当某一段仍超过块预算时再递归细分;最后通过append_utf8_bounded_slices保证切分点永远落在 UTF-8 字符边界上,不会把多字节字符拦腰截断——这一点对中文等多字节语言尤其关键(test_rag_e2e.cpp 专门用 CJK 文本回归验证了该行为)。
2.2 向量化与索引(Embedding & Indexing)
每个块经 ONNX 嵌入模型(测试中为all-MiniLM-L6-v2)编码为向量,写入 VectorStoreUSearch 向量库;同时块文本被送入 BM25Index 构建稀疏关键词索引(BM25 参数k1=1.2、b=0.75)。两者构成稠密 + 稀疏的混合索引,为后续混合检索做准备。
2.3 混合检索(Hybrid Retrieval)
查询时,管线同时执行向量相似度检索与 BM25 关键词检索,然后用RRF(Reciprocal Rank Fusion)融合两组结果。注意 rag.proto 中明确说明:返回的score是"融合后的稠密 + BM25(RRF)分数,而非原始余弦相似度",且已归一化到 0..1。
2.4 上下文组装与生成(Context Assembly & Generation)
融合后的 Top-k 块按max_context_tokens预算组装进提示模板,最终交给 LLM 生成接地回答。默认模板定义在 rag_backend.h:
Context: {context} Question: {query} Answer:RAGBackend 是整个管线的编排器(orchestrator),它接收预先创建好的 LLM 服务与嵌入服务句柄,完成"分块 → 嵌入 → 向量检索 → 自适应上下文累积 → 生成"的全流程,且所有操作线程安全。
3. 配置参数全解:proto 定义、默认值与校验逻辑
RAG 会话的核心配置集中在 idl/rag.proto 的RAGConfiguration消息中,其默认值通过rac_default注解声明,由 build_backend_config 统一映射到 C++ 内部结构。关键参数如下:
| 字段 | 类型 | 默认值 | 约束 | 说明 |
|---|---|---|---|---|
embedding_model_id | string | 必填 | — | 嵌入模型在全局模型注册表中的 ID |
llm_model_id | string | 必填 | — | 生成用 LLM 的注册表 ID |
embedding_dimension | int32 | 0(自动) | ≥1 | 0 表示自动探测嵌入模型输出维度 |
top_k | int32 | 5 | ≥1 | 检索深度(不是采样 top-k) |
score_threshold | float | 0.0 | 0.0–1.0 | 低于此分的命中被丢弃;0.0 = 不过滤 |
chunk_size | int32 | 512 | ≥1 | 每块 token 数 |
chunk_overlap | int32 | 64 | 0 ≤ overlap < chunk_size | 相邻块重叠 token 数 |
max_context_tokens | int32 | 2048 | — | 组装上下文的最大 token 预算 |
prompt_template | string | 见上文 | — | 上下文 + 问题组装模板 |
embedding_config_json | string | — | — | 嵌入模型附加配置(如 vocab 路径) |
rerank_results | bool | false | — | 是否用会话 LLM 对检索结果做 pointwise 重排 |
validate_rag_configuration 对以上参数做了严格的合法性校验:top_k < 1、score_threshold超出 [0,1] 或非有限值、chunk_size < 1、chunk_overlap < 0、chunk_overlap >= chunk_size都会返回带具体错误信息的失败结果。
一个值得注意的实现细节:proto3 的可选字段(optional)语义是"字段存在 == 调用方显式覆盖",因此显式传chunk_overlap=0(无重叠)会被忠实保留,而不会回退到结构体默认值(见 rac_rag_proto_abi.cpp 的注释说明)。
关于相似度阈值还有一个易踩的坑:源码注释明确指出(rag_backend.h),MiniLM 这类嵌入模型的余弦相似度通常不会超过约 0.5,分块又进一步降低了单块相似度,因此设置任何正数阈值都可能把真实匹配全部过滤掉。默认 0.0(全接受)正是基于这一事实的设计,用top_k来约束结果数量而非相似度门槛。
4. 查询侧能力:检索选项、多查询扩展与作用域隔离
RAGRetrievalOptions(rag.proto)定义了每次查询的检索覆盖项,未设置的字段自动继承会话级配置:
top_k:本次调用的检索深度;score_threshold:本次调用的相似度下限,显式覆盖会话级阈值(下文第 6 节有测试验证);enable_multi_query/multi_query_count:多查询扩展——把原始问题改写成多个措辞并行检索再合并结果,默认改写 3 个(1–8 范围),要求会话配置了 LLM;scope_prefix:文档 ID 前缀作用域——只保留document id以该前缀开头的块,实现多语料库隔离检索。
完整的查询入口封装在RAGQueryOptions(rag.proto)中:query必填,retrieval携带上述检索覆盖,generation复用LLMGenerationOptions(如max_output_tokens、temperature)。
此外还区分了两类查询接口:
rag.query()/rac_rag_query_proto:检索 + 生成,返回带answer与retrieved_chunks的RAGResult;rag.search()/rac_rag_search_proto:仅检索不生成,返回RAGSearchResponse块列表(见 rag.proto 的设计注释)。
5. Python SDK 实战:把rag_sample.md式的文档变成可问答的知识库
RunAnywhere Python SDK 将上述原生能力封装为rag命名空间(bindings/python/runanywhere/api/rag.py),核心对象是RagSession。参考仓库自带的 bindings/python/example/rag.py,一个完整的"打开会话 → 灌入文档 → 检索 → 问答"流程如下:
import runanywhere as ra from runanywhere import LlmOptions, ModelRef, RagConfig, RagDocument EMBEDDER = "minilm" # 嵌入模型 LLM_ID = "smollm2-135m" # 生成模型 # 1. 以文档列表形式打开 RAG 会话,并配置分块与检索参数 with ra.rag.open( ModelRef(EMBEDDER), ModelRef(LLM_ID), RagConfig(top_k=2, chunk_size=256, chunk_overlap=32), ) as session: # 2. 灌入文档(此处以 rag_sample.md 的内容为例) with open("rag_sample.md", encoding="utf-8") as f: doc_text = f.read() session.ingest([RagDocument(doc_text, id="zephyr-protocol")]) # 3. 查看索引统计 stats = session.stats() print(f"ingested {stats.document_count} documents ({stats.chunk_count} chunks)") # 4. 纯检索:不生成,只看命中 for match in session.search("How does Zephyr avoid duplicate frames?", top_k=2): print(f" {match.score:.2f} {match.text[:70]}...") # 5. 问答:检索 + 生成 result = session.query( "How does the Zephyr Protocol avoid transmitting the same data twice?", LlmOptions(max_output_tokens=96, temperature=0.2), ) print(f"answer: {result.answer.strip()}") for source in result.sources: print(f" source (score {source.score:.2f}): {source.text[:70]}...") # 6. 流式问答:逐 token 输出,结束事件携带来源 for event in session.query_stream( "What is Breeze?", LlmOptions(max_output_tokens=64, temperature=0.2) ): if event.is_token: print(event.text, end="", flush=True) elif event.is_completed and event.result is not None: print(f"\n[{len(event.result.sources)} sources]") ra.reset() # 释放原生运行时对应的高层配置类在 bindings/python/runanywhere/options.py 中定义:RagConfig(会话级top_k=5、chunk_size=512、chunk_overlap=64、similarity_threshold=None、persist_path=None)、RagRetrievalOptions(查询级top_k与similarity_threshold覆盖)、RagQueryOptions(组合retrieval与generation)。这些默认值与idl/rag.proto中的rac_default注解完全对齐,保证各语言 SDK 行为一致。
注意:Python 示例中的文档内容也可以直接采用
rag_sample.md的正文——这份测试语料本身就是一个结构良好的知识库样本。运行前需先通过initialize_from_env()或环境变量注册好嵌入模型与 LLM 的本地路径。
6. 测试如何验证这一切:内容寻址去重、阈值覆盖与作用域隔离
test_rag_e2e.cpp 是理解 RAG 行为规范的"活文档"。它用真实 ONNX 嵌入模型(all-MiniLM-L6-v2)与 GGUF LLM,通过 proto 字节 ABI(rac_rag_session_create_proto/rac_rag_ingest_proto/rac_rag_query_proto)驱动完整会话,并以rag_sample.md为语料断言多项关键行为:
6.1 内容寻址去重(Content-Addressed Dedup)
run_dedup_case(test_rag_e2e.cpp)验证:同一份文档重复 ingest 不会产生重复块。第一次 ingest 产生 N 个块,第二次用不同文档 ID 灌入相同文本,块数保持 N 不变——说明索引内部对相同内容做了哈希去重,避免了重复嵌入(re-embed)的算力浪费。这与rag_sample.md中 Zephyr 协议"同一帧永不重复传输"的内容寻址思想形成了有趣的呼应。
6.2 查询级阈值覆盖(Threshold Override)
run_threshold_override_case(test_rag_e2e.cpp)验证:会话级设置score_threshold=0.95会把所有结果过滤掉(MiniLM 余弦相似度达不到这么高);此时在查询级显式传score_threshold=0.0覆盖,结果恢复;而不传覆盖时,会话级门槛依然生效。这直接验证了第 4 节所述"查询覆盖优先、未覆盖则继承会话"的优先级规则。
6.3 文档作用域隔离(Scoped Retrieval)
run_scoping_case(test_rag_e2e.cpp)验证:向同一会话灌入zephyr:doc(Zephyr 语料)与kelp:doc(一份含独特标记词 "Kelp" 的无关文档),查询时通过scope_prefix="kelp:"限定作用域——断言返回的块全部来自 Kelp 文档,没有任何 Zephyr 块泄漏。这就是多知识库共存于同一会话时的隔离机制。
6.4 管线变体对照
基线用例之外,测试还覆盖了rerank_results=true(LLM pointwise 重排)与enable_multi_query=true(多查询扩展)两条管线变体,均要求产出"非空答案 + 至少一个检索块"。所有用例共用同一个问题:"How does the Zephyr Protocol avoid transmitting the same data twice?"——该问题与rag_sample.md中"content-addressed design means the same frame is never transmitted twice"一句形成直接的事实对应,用于验证生成的答案确实来自语料。
测试还支持通过环境变量覆盖模型路径(RAG_TEST_EMBED_MODEL、RAG_TEST_EMBED_VOCAB、RAG_TEST_LLM_MODEL、RAG_TEST_SAMPLE_DOC),当模型文件缺失时测试优雅跳过(返回 0),保证在任意构建环境下注册测试都安全(见 test_rag_e2e.cpp 与 core/tests/CMakeLists.txt)。
7. 会话生命周期与更多数据结构
除检索生成外,proto ABI 还定义了完整的会话生命周期与数据结构:
- 文档管理:
RAGDocument(rag.proto)携带调用方持有的稳定id、text、metadata与source_uri(写入每个块的source元数据);重新 ingest 相同 ID 会替换该文档的旧块。RAGDeleteRequest/RAGDeleteResponse(rag.proto)支持按文档 ID 删除,返回删除块数与未命中的 ID(非错误)。 - 统计信息:
RAGStatistics(rag.proto)返回indexed_documents、indexed_chunks、total_tokens_indexed、last_updated_ms与vector_store_size_bytes(索引占用的字节数)。 - 查询结果:
RAGResult(rag.proto)包含answer、retrieved_chunks、context_used、独立的retrieval_time_ms/generation_time_ms计时(直接测量而非相减得出)、request_id、thinking_content与 Token 用量usage。RAGSearchResult还提供start_offset/end_offset字符偏移,可回指源文档原文位置(rag.proto)。 - 流式输出:
RAGStreamEvent(rag.proto)按 token 增量推送RAG_STREAM_EVENT_KIND_TOKEN,结束时推送携带完整RAGResult的COMPLETED,错误时推送ERROR,时间戳单位为微秒。
8. 从测试语料到生产实践的要点总结
- 语料设计即测试设计:一份像
rag_sample.md这样主题集中、事实密集、含专属词汇的样本,能让检索断言(命中来源)与生成断言(答案接地)都变得可验证、可复现; - 参数默认值有讲究:
score_threshold默认 0.0、top_k默认 5、chunk_size512 /chunk_overlap64 是经 MiniLM 类嵌入模型实际特性权衡后的结果,改动前请先理解第 3 节的原理; - 去重与隔离是开箱即用:内容寻址去重避免了重复嵌入的浪费;
scope_prefix让单会话多语料库成为可能; - 查询覆盖优先于会话配置:每次调用都可以用
RAGRetrievalOptions精确覆盖top_k与score_threshold,实现细粒度的行为控制; - 验证闭环:test_rag_e2e.cpp 覆盖了基线、重排、多查询、去重、阈值覆盖、作用域隔离六类场景,是生产接入前最值得通读的规范文档。
相关资源
- 测试语料:core/tests/data/rag_sample.md
- 端到端测试:core/tests/test_rag_e2e.cpp
- 测试注册与编译宏:core/tests/CMakeLists.txt
- IDL 定义:idl/rag.proto
- 分块实现:core/src/features/rag/rag_chunker.h 与 rag_chunker.cpp
- 管线编排:core/src/features/rag/rag_backend.h
- Proto ABI 与配置校验:core/src/features/rag/rac_rag_proto_abi.cpp
- BM25 稀疏索引:core/src/features/rag/bm25_index.h
- Python SDK 示例:bindings/python/example/rag.py
- Python SDK 实现:bindings/python/runanywhere/api/rag.py 与 options.py
- AI
- 模型推理服务
- 推理引擎
- 本地部署
- 多模态
【免费下载链接】runanywhere-sdks
Production ready toolkit to run AI locally
相关推荐
all-rag-techniques:探索检索增强生成技术的实践之路
all rag techniques:探索检索增强生成技术的实践之路 在自然语言处理领域,检索增强生成(Retrieval Augmented Generati
示例工程人工智能RAG如何永久保存微信聊天记录:WeChatMsg一站式数据管理解决方案
如何永久保存微信聊天记录:WeChatMsg一站式数据管理解决方案 你是否曾担心珍贵的微信聊天记录会因手机丢失而永远消失?那些与亲友的温馨对话、重要的工作沟通、
Transformers RAG 模型完全指南:从检索增强架构、RagRetriever 配置到 RAG-Sequence/RAG-Token 生成实现
Transformers RAG 模型完全指南:从检索增强架构、RagRetriever 配置到 RAG Sequence/RAG Token 生成实现 本篇技
人工智能深度学习机器学习预训练微调NLP计算机视觉语音多模态
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考