--- # [S2-T5] 检索后处理与生成(Reranking / Prompt 工程 / LLM 调用 / 后处理)实现详解 **author:** Python 开发工程师 **source-commit:** `feae2f2e` (Merge PR #1033 release/v0.3.2) **reviewer:** 待 [S2-T7] 评审 **last-reviewed-at:** 2026-05-08 --- ## 一句话定位 本文档覆盖 MemoryBear RAG 链路的后半段:从检索结果进入系统,到最终 LLM 生成答案并输出给用户的全过程,包括重排序、Prompt 组装、多模型 LLM 调用、流式输出、工具调用及生成后处理。 ## 设计目标与适用场景 - **设计目标**:在多知识库、多检索策略(关键词 / 向量 / 混合 / GraphRAG)返回的原始结果上,通过重排序提升相关性,通过 Prompt 工程高效利用上下文,通过多提供商 LLM 封装实现高可用调用,最终输出带引用溯源、支持流式/非流式的答案。 - **适用场景**: - Agent 聊天(`app_chat_service.py` / `draft_run_service.py`) - Workflow 知识检索节点(`workflow/nodes/knowledge/node.py`) - 独立 chunk 检索 API(`chunk_controller.py`) ## 关键概念与术语表 | 术语 | 含义 | |------|------| | Rerank | 在初步召回后对 chunk 进行精细重排序 | | RedBearRerank | 基于 LangChain `BaseDocumentCompressor` 的 rerank 封装 | | Dealer | 底层检索调度器,负责混合搜索、内置 rerank、引用插入 | | KnowledgeRetrievalNode | Workflow 引擎中的知识检索节点 | | LangChainAgent | 基于 `create_agent` 的 ReAct Agent,负责工具调用循环 | | citation | 生成后处理阶段向答案文本中插入 `[ID:N]` 引用标记 | | rank_feature | 基于 tag 特征和 PageRank 的辅助排序分 | ## 实现概览(Mermaid 流程图) ``` 检索结果输入 │ ▼ ┌─────────────────┐ │ Rerank 层 │ │ A:内置混合 │ │ B:外部模型 │ │ C:RedBearRerank │ │ D:ES层封装 │ └────────┬────────┘ │ ▼ ┌─────────────────────────┐ │ Prompt 工程与上下文组装 │ │ 系统 Prompt + 技能 Prompt │ │ 知识上下文拼接 │ │ Token 预算管理 │ └────────┬────────────────┘ │ ▼ ┌─────────────────────────┐ │ LLM 调用层 (LangChainAgent)│ │ ReAct 工具调用循环 │ │ 流式/非流式 │ │ 多模态 + 深度思考 │ └────────┬────────────────┘ │ ▼ ┌─────────────────────────┐ │ 生成后处理 │ │ 引用过滤 + 下载链接 │ │ 引用插入 (embedding 匹配) │ │ JSON 结构化校验 │ └─────────────────────────┘ ``` --- ## 1. Reranking 章节 ### 1.1 是否使用显式 Rerank **是**。MemoryBear 在多处实现了 rerank,采用"多方案并存、按场景选择"策略。 ### 1.2 Rerank 方案全景 #### 方案 A:内置混合 Rerank(Dealer.rerank) **源码**:`api/app/core/rag/nlp/search.py:606-643` 核心融合公式: ``` score = tkweight * token_similarity + vtweight * vector_similarity + rank_feature ``` - `tkweight` 默认 0.3,`vtweight` 默认 0.7 - `token_similarity`:基于 rag_tokenizer 分词后的 Jaccard 风格相似度 - `vector_similarity`:query_vector 与 chunk 向量的余弦相似度 - `rank_feature`:tag 特征 TF-IDF 余弦 + PageRank,缩放 10 倍(`search.py:579-604`) - token 权重分配:`content_ltks + title_tks*2 + important_kwd*5 + question_tks*6` #### 方案 B:外部 Rerank 模型(Dealer.rerank_by_model) **源码**:`api/app/core/rag/nlp/search.py:645-666` 将向量相似度替换为外部 rerank 模型的 `similarity()` 输出,保留 token 相似度和 rank_feature。 #### 方案 C:RedBearRerank(LCEL 兼容封装) **源码**:`api/app/core/models/rerank.py:11-84` - 继承 `langchain_core.documents.BaseDocumentCompressor` - 支持 `XINFERENCE` / `GPUSTACK` → `JinaRerank` - 支持 `DASHSCOPE` → `DashScopeRerank` - 端点自动规范化:补齐 `/v1/rerank` 使用场景: - Workflow `KnowledgeRetrievalNode.rerank()`(`node.py:108-155`) - `ElasticSearchVector.rerank()`(`elasticsearch_vector.py:560-607`) - `nlp/search.py:rerank()`(`search.py:284-343`) #### 方案 D:ElasticSearchVector 层 Rerank ES Vector 初始化时注入 `reranker_config`,`rerank()` 中调用 `self.reranker.compress_documents()`。 ### 1.3 阈值与延迟 - **内置 rerank**:本地 numpy 计算,毫秒级延迟 - **外部 rerank**:网络调用,本地 Xinference <10ms,远程 DashScope 100-500ms - **相似度阈值**:`similarity_threshold` 默认 0.2,低于此值的 chunk 被过滤(`search.py:674-768`) ### 1.4 为什么没有统一使用 Cross-Encoder - Cross-Encoder 需额外部署,对小型部署不友好 - 内置 `Dealer.rerank` 在多数场景已足够 - RedBearRerank 作为可选增强,仅在显式配置 `reranker_id` 时启用 --- ## 2. Prompt 工程与上下文组装 ### 2.1 Prompt 模板组织 **目录**:`api/app/core/rag/prompts/` | 模板文件 | 用途 | |----------|------| | `ask_summary.md` | 知识库问答主 Prompt | | `citation_prompt.md` | 引用标注规范(`[ID:i]` 格式) | | `citation_plus.md` | 引用回填 Agent Prompt | | `question_prompt.md` | 文本生成问题 | | `keyword_prompt.md` | 关键词提取 | | `structured_output_prompt.md` | JSON Schema 约束 | | `cross_languages_*.md` | 跨语言查询扩展 | | `analyze_task_*.md` | 任务分析与工具选择 | **加载机制**:`api/app/core/rag/prompts/template.py:9-20`,启动时加载并缓存。 ### 2.2 上下文组装流程 **Agent 层**:`api/app/core/agent/langchain_agent.py:230-271` ```python def _prepare_messages(self, message, history, context, files): messages = [] for msg in history: if msg["role"] == "user": messages.append(HumanMessage(...)) elif msg["role"] == "assistant": messages.append(AIMessage(...)) user_content = message if context: user_content = f"参考信息:\n{context}\n\n用户问题:\n{user_content}" messages.append(HumanMessage(content=user_content)) return messages ``` ### 2.3 知识检索工具中的 Chunk 拼接 **源码**:`api/app/services/draft_run_service.py:227-255` ```python retrieve_chunks_result = knowledge_retrieval(query, kb_config) retrieval_knowledge = [i.page_content for i in retrieve_chunks_result] context = '\n\n'.join(retrieval_knowledge) return f"检索到以下相关信息:\n\n{context}" ``` - chunk 间用 `\n\n` 分隔 - 引用信息(document_id、file_name、score)由外部 `citations_collector` 收集,与上下文字符串分离 - 属于"隐式引用"策略:LLM 看不到 `[ID:N]`,引用回填在生成后完成 ### 2.4 Token 预算管理 **源码**:`api/app/core/rag/prompts/generator.py:46-80` 策略: 1. 计算总 token;未超限直接返回 2. 超限后保留 `system` + 最后一条消息,丢弃中间历史 3. 仍超限则按比例截断 system 或 user 内容 ### 2.5 System / User 分层结构 ``` system: {用户自定义 system_prompt} + {技能 Prompt} + {文档图片识别指令} user: {历史消息...} user: 参考信息:\n\n{chunks}\n\n用户问题:\n{query} ``` System Prompt 组装见 `app_chat_service.py:77-96`:先变量替换,再追加 skill_prompts。 --- ## 3. LLM 调用 ### 3.1 支持的模型与切换机制 **核心封装**:`api/app/core/rag/llm/chat_model.py:52-63` `Base` 类基于 OpenAI 兼容 API,子类覆盖: | 类名 | 提供商 | |------|--------| | `GptTurbo` | OpenAI | | `XinferenceChat` | Xinference | | `HuggingFaceChat` | HuggingFace | | `ModelScopeChat` | ModelScope | | `AzureChat` | Azure OpenAI | | `BaiChuanChat` | 百川 | | `LocalAIChat` | LocalAI | | `VolcEngineChat` | 火山引擎 | | `OpenAI_APIChat` | VLLM / OpenAI-API-Compatible | | `GPUStackChat` | GPUStack | **切换机制**:`ModelApiKeyService.get_available_api_key()` 根据 `model_id` 从数据库读取 provider/api_key/base_url/model_name,运行时动态实例化。 ### 3.2 流式 vs 非流式 **非流式**(`Base._chat()`,`chat_model.py:122-150`): - `stream=False`,返回 `(text, total_tokens)` - QWQ 推理模型强制内部走流式聚合,过滤 `` 标签 **流式**(`Base._chat_streamly()`,`chat_model.py:152-185`): - `stream=True`,yield `(delta, token_count)` - 支持 `reasoning_content` 提取 - `finish_reason == "length"` 时自动追加截断提示(中英文自适应) **Agent 流式**(`LangChainAgent.chat_stream()`): - `agent.astream_events(version="v2")` - 处理 `on_chat_model_stream` / `on_llm_stream` - 支持多模态响应解析(OpenAI + 通义千问格式) ### 3.3 超时、重试、降级 **源码**:`chat_model.py:64-89, 192-215` - 超时:`LLM_TIMEOUT_SECONDS`(默认 600s) - 重试:`LLM_MAX_RETRIES`(默认 5)+ 随机抖动延迟 - 仅对 `RATE_LIMIT` / `SERVER_ERROR` 重试 - **降级**:无自动模型降级,失败返回 `"**ERROR**: ..."` ### 3.4 函数调用 / 工具使用 **源码**:`chat_model.py:251-303, 335-436` - 最多 `max_rounds`(默认 5)轮工具调用循环 - 工具参数解析使用 `json_repair.loads()` 增强容错 - 流式工具调用:`chat_streamly_with_tools()` **Agent 工具循环**:`LangChainAgent` - `create_agent(model, tools, system_prompt)` - `max_iterations = 5 + len(tools) * 2` - 单个工具最大连续调用:`max_tool_consecutive_calls = 3` - `_wrap_tools_with_tracking()` 防循环 ### 3.5 CV 模型与序列到文本模型 **CV 模型**(`cv_model.py`):`QWenCV`、`AzureGptV4` — 用于图片/版面分析。 **序列到文本**(`sequence2txt_model.py`):`QWenSeq2txt`(带时间戳 ASR)、`GPTSeq2txt`(Whisper)— 用于音视频预处理。 --- ## 4. 生成后处理 ### 4.1 引用回填(Citation Insertion) **源码**:`api/app/core/rag/nlp/search.py:489-577` 流程: 1. 将答案按句子切分(避开代码块 ```` ``` ````) 2. 对每句话 embedding,与 chunk embeddings 计算 hybrid similarity 3. 阈值从 0.63 开始动态衰减(×0.8),最低 0.3 4. 每句最多引用 4 个 chunk,句末插入 `[ID:N]` ### 4.2 引用过滤与下载链接 **源码**:`api/app/services/draft_run_service.py:474-490` - `features_config.citation.enabled` 开关控制 - `allow_download=True` 时附加 `download_url` ### 4.3 安全过滤 当前版本无显式敏感词过滤模块。安全依赖: - LLM 提供商自带内容过滤 - `ERROR_CONTENT_FILTER` 错误码捕获 ### 4.4 输出结构化(JSON Schema) **源码**:`api/app/core/agent/langchain_agent.py:85-92` 通过 system prompt 注入 `"\n请以JSON格式输出。"` 实现(非 `response_format` API),因为 LangChain Agent 有工具时无法使用原生 API。 --- ## 5. 端到端示例 ### 场景:Agent 聊天触发知识库检索 **Step 1** — 用户提问:`"MemoryBear 的 Rerank 策略是什么?"` **Step 2** — System Prompt 组装: ``` 你是一个专业的 AI 知识库助手,名为 Miss R。 任务:根据知识库中的信息回答用户问题。 要求:不要编造信息;使用 Markdown;用用户提问的语言回答。 ``` (来自 `ask_summary.md`) **Step 3** — LLM 判断调用 `knowledge_retrieval_tool` 工具内部: ```python retrieve_chunks_result = knowledge_retrieval(query, kb_config) context = '\n\n'.join([i.page_content for i in retrieve_chunks_result]) return f"检索到以下相关信息:\n\n{context}" ``` **Step 4** — 若配置 `reranker_id`,执行 RedBearRerank: ```python reranker = RedBearRerank(RedBearModelConfig(...)) reranked_docs = list(reranker.compress_documents(documents, query)) ``` **Step 5** — Agent 组装消息并调用 LLM: ``` system: 你是一个专业的 AI 知识库助手... user: 参考信息:\n\nChunk 0...\n\nChunk 1...\n\n用户问题:\nMemoryBear 的 Rerank 策略是什么? ``` **Step 6** — 输出后处理: ```python filtered_citations = _filter_citations(features_config, citations_collector) ``` 最终返回:content + citations(含 document_id、file_name、score、可选 download_url)。 --- ## 6. 关键源码索引 | 功能 | 文件 | 类/函数 | 行号 | |------|------|---------|------| | Rerank 封装 | `api/app/core/models/rerank.py` | `RedBearRerank` | 11-84 | | 内置混合 Rerank | `api/app/core/rag/nlp/search.py` | `Dealer.rerank` | 606-643 | | 外部模型 Rerank | `api/app/core/rag/nlp/search.py` | `Dealer.rerank_by_model` | 645-666 | | rank_feature | `api/app/core/rag/nlp/search.py` | `_rank_feature_scores` | 579-604 | | 独立 rerank | `api/app/core/rag/nlp/search.py` | `rerank()` | 284-343 | | 知识检索入口 | `api/app/core/rag/nlp/search.py` | `knowledge_retrieval()` | 36-147 | | ES Vector rerank | `api/app/core/rag/vdb/elasticsearch/elasticsearch_vector.py` | `ElasticSearchVector.rerank` | 560-607 | | Workflow 节点 rerank | `api/app/core/workflow/nodes/knowledge/node.py` | `KnowledgeRetrievalNode.rerank` | 108-155 | | Workflow 执行 | `api/app/core/workflow/nodes/knowledge/node.py` | `KnowledgeRetrievalNode.execute` | 303-378 | | LLM 基类 | `api/app/core/rag/llm/chat_model.py` | `Base` | 52-319 | | 流式 LLM | `api/app/core/rag/llm/chat_model.py` | `_chat_streamly` | 152-185 | | 工具调用 | `api/app/core/rag/llm/chat_model.py` | `chat_with_tools` | 251-303 | | 流式工具调用 | `api/app/core/rag/llm/chat_model.py` | `chat_streamly_with_tools` | 335-436 | | 错误分类 | `api/app/core/rag/llm/chat_model.py` | `_classify_error` | 69-89 | | CV 模型 | `api/app/core/rag/llm/cv_model.py` | `QWenCV`, `AzureGptV4` | 1-497 | | 音频转录 | `api/app/core/rag/llm/sequence2txt_model.py` | `QWenSeq2txt`, `GPTSeq2txt` | 1-215 | | Prompt 加载 | `api/app/core/rag/prompts/template.py` | `load_prompt` | 9-20 | | Prompt 生成器 | `api/app/core/rag/prompts/generator.py` | `message_fit_in` 等 | 1-744 | | Agent 封装 | `api/app/core/agent/langchain_agent.py` | `LangChainAgent` | 26-641 | | Agent 消息准备 | `api/app/core/agent/langchain_agent.py` | `_prepare_messages` | 230-271 | | 知识检索工具 | `api/app/services/draft_run_service.py` | `create_knowledge_retrieval_tool` | 195-263 | | 引用过滤 | `api/app/services/draft_run_service.py` | `_filter_citations` | 474-490 | | 聊天服务 | `api/app/services/app_chat_service.py` | `agnet_chat` | 43-239 | | 流式聊天 | `api/app/services/app_chat_service.py` | `agnet_chat_stream` | 340-550 | | 引用插入 | `api/app/core/rag/nlp/search.py` | `Dealer.insert_citations` | 489-577 | --- ## 7. 配置项与可调参数 **环境变量**: | 变量 | 默认值 | 说明 | |------|--------|------| | `LLM_TIMEOUT_SECONDS` | 600 | LLM 超时 | | `LLM_MAX_RETRIES` | 5 | 最大重试 | | `LLM_BASE_DELAY` | 2.0 | 重试基础延迟 | **知识检索配置**: | 配置项 | 默认值 | 说明 | |--------|--------|------| | `retrieve_type` | `participle` | participle/semantic/hybrid/graph | | `similarity_threshold` | 0.2 | 关键词相似度阈值 | | `vector_similarity_weight` | 0.3 | 向量权重 | | `top_k` | 4 | 单次检索 chunk 数 | | `reranker_id` | `None` | Rerank 模型 ID | | `reranker_top_k` | 4 | Rerank 后最终返回数 | **Agent 参数**: | 配置项 | 默认值 | 说明 | |--------|--------|------| | `max_iterations` | `5 + len(tools) * 2` | Agent 最大迭代 | | `max_tool_consecutive_calls` | 3 | 单工具最大连续调用 | | `max_rounds` | 5 | LLM 工具调用最大轮数 | | `temperature` | 0.7 | 生成温度 | | `max_tokens` | 2000 | 最大生成 token | | `json_output` | `False` | 强制 JSON 输出 | | `deep_thinking` | `False` | 深度思考 | --- ## 8. 边界条件与已知限制 1. **外部 Rerank 延迟高**:RedBearRerank 调用 Jina/DashScope API,无本地缓存。 2. **Token 裁剪较粗糙**:`message_fit_in` 丢弃中间历史,可能丢失上下文;按比例截断可能切断语义。 3. **引用回填非 LLM 原生**:基于 embedding 相似度匹配,表述不同可能漏引。 4. **JSON 输出兼容性差**:通过 system prompt 注入实现,可靠性低于原生 `response_format`。 5. **无模型降级**:LLM 失败返回错误文本,不自动切换备用模型。 6. **混合检索融合简单**:仅去重取并集,无 RRF 或加权分数融合。 7. **GraphRAG 结果前置**:始终 `insert(0, ...)`,优先级最高但无分数参与 rerank。 --- ## 9. 优化建议与未来扩展点 1. **Rerank 缓存**:对高频 query 做 LRU 缓存,降低外部 API 成本。 2. **引用增强**:将 `citation_prompt.md` 注入 system prompt,让 LLM 生成阶段就输出 `[ID:N]`。 3. **Token 预算精细化**:引入 `tiktoken` 精确计数,实现滑动窗口历史管理。 4. **模型降级**:在 `Base.chat()` 中增加 fallback 模型链。 5. **混合检索 RRF**:在 ES 查询层面实现 Reciprocal Rank Fusion。 6. **流式引用**:在 `on_tool_end` 事件中实时 emit citation 元数据。 7. **输出校验中间件**:对 `json_output=True` 增加 JSON Schema 强制校验层。 --- 以上为 [S2-T5] 初版全文,请评审。