OpenViking 检索机制深度解析:意图分析 + 层级检索 + Rerank 的三阶段召回管线
【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking
OpenViking 面向 AI Agent 提供了统一的自进化上下文数据库(Unify Agent Memory, Knowledge RAG and Skills),其检索机制是连接用户查询与记忆、资源、技能三层上下文的枢纽。本文基于 docs/zh/concepts/07-retrieval.md 展开,系统讲解find()与search()的差异、IntentAnalyzer 意图分析、HierarchicalRetriever 层级递归检索与 Rerank 精排的完整调用链,并结合 openviking/retrieve/ 目录下的源码实现与配置指南,给出可直接落地的配置示例。读完本文,你将掌握 OpenViking 检索的完整数据流、关键参数调优方法,以及如何用少量配置让检索同时获得“高召回”与“精排序”。
一、整体架构:三阶段检索管线
OpenViking 的检索机制采用经典的三阶段设计:意图分析 → 层级检索 → Rerank。原始查询首先被拆解为多个带类型与优先级的子查询,随后按上下文类型确定根目录并递归搜索目录树,最后在 THINKING 模式下由精排模型对候选结果重新打分。
查询 → 意图分析 → 层级检索 → Rerank → 结果 ↓ ↓ ↓ TypedQuery 目录递归 精排评分该流程的每一环都有独立模块承担,分布在 openviking/retrieve/ 目录下:
intent_analyzer.py:IntentAnalyzer,负责把会话上下文交给 LLM 生成查询计划;hierarchical_retriever.py:HierarchicalRetriever,用优先队列实现目录树递归检索与分数传播;retrieval_stats.py:检索统计收集器,记录每次查询的结果数、分数与延迟;context_assembler/:上下文组装管线,把检索结果按预算、层级组装成最终注入 Agent 的上下文。
此外,核心数据结构(TypedQuery、QueryPlan、MatchedContext、QueryResult、FindResult、ThinkingTrace等)定义在 openviking_cli/retrieve/types.py 中,供检索服务与 SDK 共享。
二、两个入口:find() 与 search()
OpenViking 面向客户端提供两个检索入口,分别对应“简单查询”与“复杂任务”两类场景。两者在是否依赖会话上下文、是否使用 LLM 意图分析、查询数量与延迟上有本质区别:
| 特性 | find() | search() |
|---|---|---|
| 会话上下文 | 不需要 | 需要 |
| 意图分析 | 不使用 | 使用 LLM 分析 |
| 查询数量 | 单一查询 | 0-5 个 TypedQuery |
| 延迟 | 低 | 较高 |
| 适用场景 | 简单查询 | 复杂任务 |
从服务端实现看,二者在 openviking/service/search_service.py 中分属两条路径:find()(L139)是无会话的语义搜索,直接以原始查询执行向量召回;而search()(L91)在传入session且启用意图分析时,会先调用session.get_context_for_search(query)获取会话压缩摘要,再进入viking_fs.search。该开关由配置项retrieval.enable_intent控制,关闭后search()会跳过会话加载与IntentAnalyzer,退化为与无会话搜索相同的原始查询路径(见 retrieval_config.py)。
SDK 层的两个异步入口位于 sdk/python/openviking_sdk/client.py:find()请求/api/v1/search/find(L1361),search()请求/api/v1/search/search(L1384),并支持target_uri、limit、image与FindOptions/SearchOptions扩展参数。
使用示例
# find(): 简单查询,无需会话 results = await client.find( query="OAuth 认证", target_uri="viking://resources/", ) # search(): 复杂任务(需要会话上下文) session_info = await client.create_session() results = await client.search( query="帮我创建一个 RFC 文档", session_id=session_info["session_id"], )create_session()同样来自 SDK(L1487),请求POST /api/v1/sessions,返回的session_id即search()的会话凭证。需要说明:search()返回的FindResult由total、memories、resources、skills四部分组成,其中total在FindResult.__post_init__中被自动计算为三类上下文数量之和(见 openviking_cli/retrieve/types.py)。
三、意图分析:从一句话到 0-5 个 TypedQuery
3.1 IntentAnalyzer 的职责
IntentAnalyzer位于 openviking/retrieve/intent_analyzer.py,其核心职责有三条(源码 docstring 明确声明):
- 整合会话上下文(压缩摘要 + 最近消息 + 当前消息);
- 调用 LLM 分析意图;
- 生成面向 memory / resources / skill 的多个 TypedQuery。
它通过get_openviking_config().get_query_planner()获取规划模型;未配置query_planner时回退到vlm。该阶段使用的模型可由query_planner配置项单独指定,意图分析会完整消费以下三部分输入:
- 会话压缩摘要(
compression_summary,超过 30000 字符会被截断,对应MAX_COMPRESSION_SUMMARY_CHARS,约为 10000 token 的预算); - 最近 5 条消息(
messages[-5:],max_recent_messages默认值为 5); - 当前查询(
current_message)。
3.2 输出:TypedQuery 与 QueryPlan
意图分析的输出是QueryPlan,其中包含 0-5 个TypedQuery。两者的字段定义在 openviking_cli/retrieve/types.py:
@dataclass class TypedQuery: query: str # 重写后的查询 context_type: ContextType # MEMORY/RESOURCE/SKILL intent: str # 查询目的 priority: int # 1-5 优先级 target_directories: List[str] # LLM 定位的目录 URI(可空)源码中的ContextType是字符串枚举:MEMORY = "memory"、RESOURCE = "resource"、SKILL = "skill"(types.py#L16-L21)。TypedQuery.priority默认值为 3,范围 1-5,数字越小优先级越高。
QueryPlan额外携带session_context(会话上下文摘要)与reasoning(LLM 推理过程),前者会作为session_context返回给上层,后者可透出到FindResult.to_dict()的query_plan字段,用于检索可观测性。
从源码实现看,LLM 返回的 JSON 被parse_json_from_response解析后,若顶层是数组会被包装为{"reasoning": "", "queries": [...]};每个查询项的context_type通过ContextType(...)严格校验,非法值回退为RESOURCE,priority缺失时取默认值 3(intent_analyzer.py#L89-L123)。
3.3 查询风格与特殊情况
意图分析会根据查询的语言形态决定context_type,官方文档给出如下风格约定:
| 类型 | 风格 | 示例 |
|---|---|---|
| skill | 动词开头 | "创建 RFC 文档"、"提取 PDF 表格" |
| resource | 名词短语 | "RFC 文档模板"、"API 使用指南" |
| memory | "用户XX" | "用户的代码规范偏好" |
两种特殊情况需要特别注意:
- 0 个查询:闲聊、问候等不需要检索的场景。此时
QueryPlan.queries为空,检索管线直接短路,避免不必要的记忆注入与 token 消耗——这正是推荐用轻量小模型承担意图分析的核心价值之一。 - 多个查询:复杂任务可能需要技能 + 资源 + 记忆三类上下文,例如“帮我创建一个 RFC 文档”可能同时生成 resource 查询(RFC 模板)与 skill 查询(文档创建技能)。
3.4 query_planner 配置:用小模型承担检索规划
意图分析默认走vlm,但官方强烈建议为search()单独配置一个轻量的本地 query planner 模型,以降低延迟与成本。推荐模型为 Ollama 上的guoxuter/ov_intent_analysis_sft:v7_q8(基于 Qwen3.5-0.8B 微调),此前版本v4_q8仍作为可选项支持。
ollama pull guoxuter/ov_intent_analysis_sft:v7_q8然后在 OpenViking 配置中追加(完整配置说明见 query_planner 配置):
{ "query_planner": { "provider": "litellm", "model": "ollama/guoxuter/ov_intent_analysis_sft:v7_q8", "api_base": "http://127.0.0.1:11434", "temperature": 0.0, "timeout": 60, "extra_request_body": { "think": false } } }一个值得关注的实现细节:OpenViking 内置了按模型映射的 prompt 选择机制。QUERY_PLANNER_PROMPT_BY_MODEL表(intent_analyzer.py#L24-L27)把ollama/guoxuter/ov_intent_analysis_sft:v7_q8与v4_q8分别映射到retrieval.ov_intent_analysis_sft_v7与retrieval.ov_intent_analysis_sft_v4内置 prompt;未映射的模型继续使用默认的retrieval.intent_analysisprompt。因此使用官方推荐模型时不需要替换 prompt 文件,也无需设置prompts.templates_dir。该方案让 0.8B 级小模型承担检索规划,同时保留更强的vlm处理语义提取、记忆提取与多模态内容。
四、层级检索:用优先队列递归搜索目录树
4.1 五步流程
HierarchicalRetriever位于 openviking/retrieve/hierarchical_retriever.py,实现“目录树优先队列 + 分数传播 + 收敛检测”的递归检索。其整体流程为:
Step 1: 根据 context_type 确定根目录 ↓ Step 2: 全局向量搜索定位起始目录 ↓ Step 3: 合并起始点 + Rerank 评分 ↓ Step 4: 递归搜索(优先队列) ↓ Step 5: 转换为 MatchedContext结合源码,retrieve()(L101-L351)的实现比文档描述更细致:Step 1 在target_dirs显式指定时直接作为根目录集合,否则调用default_target_directories(ctx, context_type=...)(定义于 openviking/core/retrieval_targets.py)按租户与 peer 解析默认目录;Step 2 的全局搜索限定level=[0, 1](即只召回目录层 L0/L1),步长取max(limit, GLOBAL_SEARCH_TOPK)。
4.2 根目录映射
| context_type | 根目录 |
|---|---|
| MEMORY | viking://~/memories |
| RESOURCE | viking://resources |
| SKILL | viking://~/skills |
实际解析时,default_target_directories会结合用户身份做更精细的展开(retrieval_targets.py#L60-L83):MEMORY 展开为{user_root}/memories(有 peer 时追加{user_root}/peers/{peer}/memories),RESOURCE 展开为viking://resources、{user_root}/resources与 peer 资源目录,SKILL 展开为viking://agent/skills与用户技能目录并去重。
4.3 递归搜索算法与分数传播
递归搜索的核心逻辑在_recursive_search()(L421-L588),文档给出的伪代码与源码高度一致:
while dir_queue: current_uri, parent_score = heapq.heappop(dir_queue) # 搜索子节点 results = await search(parent_uri=current_uri) for r in results: # 分数传播 final_score = score_propagation_alpha * embedding_score + (1 - score_propagation_alpha) * parent_score if final_score > threshold: collected.append(r) if not r.is_leaf: # 目录继续递归 heapq.heappush(dir_queue, (r.uri, final_score)) # 收敛检测 if topk_unchanged_for_3_rounds: break从源码可以看到几个文档之外的工程细节:
- 并行扇出:每轮从优先队列取出
MAX_PARALLEL_CHILD_SEARCHES = 4个目录,用asyncio.gather并发搜索子节点,控制对远程向量库的并发请求量(L495-L513); - 分数传播公式:
final_score = alpha * score + (1 - alpha) * current_score if current_score else score——当父节点分数为 0(如显式根目录)时直接使用子节点自身分数(L531-L533); - 只对目录递归:
r.get("level", 2) != 2才入队继续展开,L2 文件是终端命中(L555-L556); - 按 URI 去重:同一 URI 只保留最高分候选(
collected_by_uri字典); - 双重收敛检测:除了“topk 连续 3 轮不变”,还有“候选池规模停滞 3 轮”的
stagnant_rounds兜底,二者任一达到MAX_CONVERGENCE_ROUNDS = 3即提前终止(L558-L581)。
4.4 关键参数
| 参数 | 值 | 说明 |
|---|---|---|
retrieval.score_propagation_alpha | 1.0 | 分数传播混合中子节点自身分数的权重;1.0表示仅使用子节点自身分数,忽略父节点分数 |
MAX_CONVERGENCE_ROUNDS | 3 | 收敛检测轮数 |
GLOBAL_SEARCH_TOPK | 10 | 全局搜索候选数 |
score_propagation_alpha是唯一暴露到配置中的检索行为参数,实际读取路径为retrieval_config.py→HierarchicalRetriever.__init__的self.score_propagation_alpha(hierarchical_retriever.py#L81-L83)。其完整取值语义(retrieval_config.py#L19-L28):
1.0:忽略父节点分数,只使用子节点自身语义相似度(默认);0.5:与父节点分数等权混合;0.0:只使用父节点分数。
其余两个参数是源码中的类常量:MAX_CONVERGENCE_ROUNDS = 3、GLOBAL_SEARCH_TOPK = 10(L56-L59)。同处还有两个工程常量值得了解:DIRECTORY_DOMINANCE_RATIO = 1.2(目录分数须超过子节点最高分)与MAX_PARALLEL_CHILD_SEARCHES = 4。
4.5 两种检索模式与分数阈值
RetrieverMode枚举定义在 hierarchical_retriever.py#L48-L50:
- QUICK(快速模式):
mode未指定且未配置 rerank 时自动选择,只做一次直接的向量召回(search_in_tenant),不进行递归展开与分数传播,延迟最低; - THINKING(思考模式):配置了 rerank 时默认启用,执行完整的多阶段流程(全局搜索定位起始点 → 递归搜索 → Rerank 精排),
search()默认走此模式。
此外,retrieve()支持score_threshold与score_gte两个阈值参数:前者可覆盖配置阈值,后者控制比较符(True用>=,False用>),相关测试见 tests/retrieve/test_hierarchical_retriever_rerank.py。
五、Rerank 策略:THINKING 模式下的精排
5.1 触发条件与回退机制
Rerank 在 THINKING 模式下对候选结果精排,触发需同时满足:
- 配置了 Rerank AK/SK(或 OpenAI 兼容凭据);
- 使用 THINKING 模式(
search()默认)。
若 rerank 返回无效结果或 API 调用失败,会自动回退到向量分数。回退逻辑在_rerank_scores()(L371-L419)中实现:调用异常、返回空结果、长度不匹配三种情况都会logger.warning后原样返回fallback_scores;即使部分文档为空串,也会先过滤再调用,空文档的位置保留原向量分数。对应测试test_rerank_scores_preserves_fallbacks_for_empty_documents(tests/retrieve/test_hierarchical_retriever_rerank.py#L282)验证了这一行为。
5.2 评分方式
if rerank_client and mode == THINKING: scores = rerank_client.rerank_batch(query, documents) else: scores = [r["_score"] for r in results] # 向量分数Rerank 打分被用在两处(与文档描述一致):
- 起始点评估:对全局搜索召回的所有目录候选(
global_results)按 abstract 重新打分,作为递归搜索的入口优先级; - 递归搜索:对每层子节点的 abstract 重新打分,再与父节点分数做传播混合。
一个额外的工程细节:当max_input_tokens > 0时,_rerank_scores会对 query 与 document 做 token 预算截断——query 占用max_input_tokens * 3 // 4,其余预算分给 document,超长文本“保留开头和结尾”式截断(L388-L396)。
5.3 后端支持
| 后端 | 模型 |
|---|---|
| Volcengine | doubao-seed-rerank |
从 openviking/models/rerank/ 目录看,Rerank 客户端实际支持四类统一派发:volcengine_rerank.py(VikingDB)、cohere_rerank.py(Cohere)、openai_rerank.py(OpenAI 兼容接口)与litellm_rerank.py(LiteLLM 聚合)。RerankClient.from_config(rerank_config)会按配置的 provider 自动选择实现(hierarchical_retriever.py#L88-L99)。
官方文档主推的火山引擎配置(rerank 配置):
{ "rerank": { "provider": "vikingdb", "ak": "your-access-key", "sk": "your-secret-key", "model_name": "doubao-seed-rerank", "model_version": "251028" } }OpenAI 兼容提供方(如 DashScope)示例:
{ "rerank": { "provider": "openai", "api_key": "your-api-key", "api_base": "https://dashscope.aliyuncs.com/compatible-api/v1/reranks", "model": "qwen3-rerank", "timeout": 120, "max_input_tokens": 2048, "threshold": 0.1 } }关键参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
provider | str | "vikingdb"、"cohere"或"openai"。省略时基于字段自动识别。 |
ak/sk | str | VikingDB Access Key / Secret Key(仅vikingdb提供方使用) |
model_name | str | 模型名称(仅vikingdb提供方使用,默认:doubao-seed-rerank) |
api_key | str | API Key(用于openai或cohere提供方) |
api_base | str | 接口地址(用于openai提供方) |
model | str | 模型名称(用于openai提供方) |
timeout | float | OpenAI 兼容 provider 的 HTTP 超时(默认30.0,本地冷启动 rerank 可调大) |
max_input_tokens | int | 每个 query-document 对的最大估算 token 数;0表示不截断(默认0) |
threshold | float | 分数阈值0.0-1.0,低于此值的结果被过滤(默认0.1) |
注意:如果未配置 Rerank,搜索仅使用向量相似度(QUICK 模式),此时threshold退化为 0。
六、检索结果的数据结构
6.1 MatchedContext
递归搜索与精排结束后,候选结果经_convert_to_matched_contexts()(L590-L658)转换为统一的MatchedContext:
@dataclass class MatchedContext: uri: str # 资源 URI context_type: ContextType is_leaf: bool # 是否文件 abstract: str # L0 摘要 score: float # 最终分数源码中该数据类的字段更完整:level(0=L0 摘要、1=L1 概览、2=L2 文件)、category、match_reason、search_tags(types.py#L276-L289)。转换时有几个值得留意的行为:
- 分数混合:THINKING 模式下若
hotness_alpha > 0,最终分数 =(1 - alpha) * 语义分数 + alpha * hotness_score,其中 hotness 由active_count与updated_at经hotness_score()计算(memory_lifecycle.py),随后按混合分数重新排序; - L0/L1 预览净化:L0/L1 记录的 abstract 会经
body_for_preview()只保留正文,剔除完整 OKF 文档内容;L2 用户 Markdown 保持原样; - URI 后缀重建:
_append_level_suffix按层级为展示 URI 补上.abstract.md(L0)或.overview.md(L1)后缀(L660-L672); - 阈值校验:
_finite_score会把 inf/nan 分数收敛为 0.0,避免向量库异常分数污染排序。
6.2 FindResult
find()与search()最终都返回FindResult:
@dataclass class FindResult: memories: List[MatchedContext] resources: List[MatchedContext] skills: List[MatchedContext] query_plan: Optional[QueryPlan] # search() 时有 query_results: Optional[List[QueryResult]] total: intFindResult同时实现了__iter__(按 memories → resources → skills 顺序迭代全部命中)与to_dict(include_provenance=False)(types.py#L344-L410):query_plan输出reasoning与各 TypedQuery 明细;include_provenance=True时额外输出provenance,内含每个查询的searched_directories、命中层级(L0/L1/L2前缀)、match_reason与thinking_trace,用于检索过程的可视化与审计。ThinkingTrace是线程安全的队列式事件记录器(queue.Queue),可输出事件列表、统计信息(搜索目录数、候选收集/排除数、收敛轮数)与简单消息列表(types.py#L131-L235)。
七、检索可观测性:统计与调优依据
OpenViking 为检索提供了两层可观测能力:
- RetrievalStatsCollector(openviking/retrieve/retrieval_stats.py):在
retrieve()结束处调用record_query()(hierarchical_retriever.py#L338-L345),按 context_type 累计查询次数、命中数、分数分布、延迟(avg/max),并标记rerank_used,可据此评估某类上下文的召回质量与 Rerank 的收益; - ThinkingTrace:
QueryResult.thinking_trace保留完整的搜索决策链,配合to_dict(include_provenance=True)可在应用层做召回过程审计。
调优时建议按“先看 stats、再调参数”的顺序:若某类上下文的avg_latency_ms明显偏高,可考虑为search()配置本地 query planner 小模型;若希望高频访问或最近更新的上下文获得排序提升,可将retrieval.hotness_alpha从默认0.0调高(此时最终分数不再是纯语义相似度);若递归展开过深导致延迟上升,可调整limit或依赖收敛检测提前终止。完整配置项及默认值见 docs/zh/guides/01-configuration.md:
{ "retrieval": { "hotness_alpha": 0.0, "score_propagation_alpha": 1.0, "recall_intent_timeout_s": 5.0, "recall_rewrite_timeout_s": 30.0 } }| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
hotness_alpha | float | hotness 分数在最终召回分数中的混合权重。0.0关闭 hotness boost,1.0只使用 hotness。范围0.0-1.0。 | 0.0 |
score_propagation_alpha | float | 层级检索中子节点自身分数权重。1.0忽略父节点分数;0.5等权混合;0.0只使用父节点分数。 | 1.0 |
recall_intent_timeout_s | float | 会话感知查询扩展超时,超时回退为用户原查询 | 5.0 |
recall_rewrite_timeout_s | float | digest 重写超时,超时后digest为空并照常返回rendered | 30.0 |
八、调用链总结与延伸阅读
把本文内容串成一条完整的调用链:SDKclient.search()(sdk/python/openviking_sdk/client.py)→ HTTP/api/v1/search/search→SearchService.search()(openviking/service/search_service.py)→ 会话上下文加载 →IntentAnalyzer.analyze()(openviking/retrieve/intent_analyzer.py)生成 0-5 个 TypedQuery →HierarchicalRetriever.retrieve()按 QUICK/THINKING 模式执行向量召回 + 递归搜索 + Rerank →_convert_to_matched_contexts()组装MatchedContext→ 汇总为FindResult返回。find()则跳过意图分析,直接走向量召回路径。
相关的源码与测试可继续深入探索:
- 检索实现:openviking/retrieve/hierarchical_retriever.py、openviking/retrieve/intent_analyzer.py、openviking/retrieve/retrieval_stats.py;
- 数据结构:openviking_cli/retrieve/types.py;
- Rerank 客户端:openviking/models/rerank/;
- 服务入口:openviking/service/search_service.py、openviking/core/retrieval_targets.py;
- 测试用例:tests/retrieve/test_hierarchical_retriever_rerank.py、tests/retrieve/test_intent_analyzer_query_planner.py;
- 关联文档:架构概述、存储架构、上下文层级、上下文类型、配置指南。
理解了三阶段管线的每个环节,你就能根据实际任务复杂度、延迟预算与排序精度要求,在find()/search()、QUICK/THINKING 模式、query planner 与 rerank 模型之间做出合理取舍,让 OpenViking 的记忆、知识与技能检索真正服务于 Agent 任务。
【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考