docs(rag): add MemoryBear RAG implementation docs v1.0
Some checks failed
Sync to Gitee / sync (push) Has been cancelled
Some checks failed
Sync to Gitee / sync (push) Has been cancelled
Submit the formed RAG documentation set produced across Sprint-1/2/3 (WS-12 through WS-26) under docs/rag/. Includes: - README.md / INDEX.md: landing + total index (responsibility matrix, review verdicts, dual-link to source issues) - overview/: full-pipeline architecture (4 .mmd diagrams), 11-stage boundary contracts, doc map, source-code inventory - pipeline/: 5 deep-dives (Loader/Parser/Chunking, Embedding, VDB & retrieval, GraphRAG, Rerank/Prompt/LLM) - graphrag/, end-to-end/: v1.0 formal versions with full source retained as reference - evolution/: 11 architecture-refactor proposals, 6-direction roadmap, capability map - review/: S3-T1 / S3-T2 final reviews, S2-T7 final summary - _indexes/: glossary (81 terms), source->doc reverse index, chart index - _release/: v1.0-RC1 release manifest, versioning convention, ops & freshness plan - _meta/README.md: placeholder noting WS-12 governance assets gap Aggregate review score 92.6/100 (8/8 PASS, 31/31 source-code spot checks hit). The legacy docs/ ignore in .gitignore is narrowed to docs/* with an explicit allowlist for docs/rag/. Refs: WS-26 Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
434
docs/rag/evolution/architecture-refactor-suggestions.md
Normal file
434
docs/rag/evolution/architecture-refactor-suggestions.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# [S3-T1] MemoryBear RAG 代码架构改造建议
|
||||
|
||||
**Author**: AI 知识库解决方案专家
|
||||
**Source-commit**: 工作分支 `agent/ai/f8de881a`(基于 `feae2f2e`)
|
||||
**Reviewer**: 待 [S3-T3] 终审
|
||||
**Last-reviewed-at**: 2026-05-08
|
||||
|
||||
---
|
||||
|
||||
## 0. 一页摘要:现状评估
|
||||
|
||||
### 0.1 三个优点(值得保留与放大)
|
||||
|
||||
1. **链路完整、特性丰富**:覆盖了从 11 类文档解析(`rag/app/naive.py:508-738`,按扩展名 if/elif 分发)→ Embedding(10+ Provider)→ Hybrid 检索(BM25 + 向量)→ GraphRAG(light/general 双模式)→ Rerank → Prompt 组装 → 流式 LLM 生成的端到端能力。在国内同类开源项目中链路完整度领先。
|
||||
2. **多 Provider 抽象初步成型**:`rag/llm/chat_model.py:52 Base` + `rag/vdb/vector_base.py:9 BaseVector` 已具备抽象基类雏形;`rag/models/embedding.py RedBearEmbeddings` 通过 LangChain 的 `Embeddings` 接口屏蔽了 OpenAI / DashScope / Volcano / Ollama / Bedrock 等 7 类 provider。多模型切换代价较低。
|
||||
3. **GraphRAG 与向量检索的双轨设计**:`rag/common/settings.py:9-10` 通过 `retriever`(Dealer)+ `kg_retriever`(KGSearch)两个全局单例并行存在,应用层(`workflow/nodes/knowledge/node.py`)可在 PARTICIPLE / SEMANTIC / HYBRID / GRAPH 四种检索模式间切换,灵活度高,是 MemoryBear 区别于通用 RAG 的核心特色。
|
||||
|
||||
### 0.2 五个痛点(基于 S1-T3 Gap 报告 + 源码核验)
|
||||
|
||||
1. **抽象层不统一,存在双轨甚至三轨实现**:
|
||||
- **Embedding 双轨**:`rag/models/embedding.py RedBearEmbeddings`(LangChain,新,被 ES Vector 用)vs `rag/llm/embedding_model.py OpenAIEmbed/QWenEmbed/...`(遗留,被 GraphRAG `utils.py:320` 与 Dealer `nlp/search.py:365-373` 用)。**两条路径接口不兼容**:前者 `embed_documents(texts)`、后者 `encode(texts)` 返回 `(np.array, total_tokens)`。
|
||||
- **Rerank 三轨**:模块级 `rerank()`(`workflow/nodes/knowledge/node.py:284`,**第 327 行残留 `print(reranked_docs)` 调试语句**)、节点级 `KnowledgeRetrievalNode.rerank()`(`node.py:108-155`,与前者逻辑高度重复)、Dealer 内置融合 `Dealer.rerank()`(`nlp/search.py:606-643`,token+vector+rank_feature 加权)。三套互不知晓彼此存在。
|
||||
- **VDB 抽象有名无实**:`vector_base.py:9 BaseVector` 仅定义了 9 个抽象方法,但唯一实现为 `ElasticSearchVector`,且 `node.py:14`、`tasks.py` 直接 import 具体类 `ElasticSearchVectorFactory` 绕过基类,抽象层失效。
|
||||
|
||||
2. **配置散落,无中心化治理**:`os.environ.get` / `os.getenv` 在 `rag/` 目录下出现 **58 次**,分布在 48 个文件。例如 `LLM_TIMEOUT_SECONDS`/`LLM_MAX_RETRIES`(`chat_model.py:54-58`)、`MAX_CONCURRENT_CHATS`(`graphrag/utils.py:41`)、`ELASTICSEARCH_HOST/PORT/USERNAME/PASSWORD/REQUEST_TIMEOUT/MAX_RETRIES`(`elasticsearch_vector.py:685-707`)、`MINERU_EXECUTABLE/APISERVER/OUTPUT_DIR/BACKEND/DELETE_OUTPUT`(`naive.py:46-60`)、OCR/Layout 系列(`deepdoc/vision/*`)等无统一 schema、无类型校验、无文档可查。运维难以定位"哪个变量影响哪条链路"。
|
||||
|
||||
3. **可观测性等同于零**:`requirements*.txt` 中 **没有任何** `opentelemetry / prometheus / sentry / jaeger / datadog / statsd` 依赖;355 处 `logger.*` / `logging.*` 调用全为本地日志,无 trace_id 透传、无 metric 导出、无 P50/P95 实时统计。README 里宣称的"P50/P95"指标在代码中无任何采集落点,业务方排障必须捞日志手工聚合。
|
||||
|
||||
4. **资源/状态共享导致单测与并发受阻**:
|
||||
- `rag/common/settings.py:24` 在模块 import 时立即执行 `init_settings()`,创建 `docStoreConn = ESConnection()` / `retriever = Dealer(...)` / `kg_retriever = KGSearch(...)` **进程级全局单例**。任何 `from app.core.rag.common.settings import retriever` 都会触发 ES 连接,单元测试无法 stub。
|
||||
- `KnowledgeRetrievalNode.get_reranker_model()`(`node.py:157-193`)每次 `rerank` 调用都重新查 DB → 实例化 `RedBearRerank`,热路径上反复读库。
|
||||
- GraphRAG 用 Redis 做 Embedding 缓存(`graphrag/utils.py:115-134 get_embed_cache/set_embed_cache`),但 ES VDB 入库/检索路径**完全没有缓存**(`elasticsearch_vector.py:55-63`),同一 query 重复打 Embedding API。
|
||||
|
||||
5. **入口分发与扩展点用 if/elif 硬编码,违反开闭原则**:
|
||||
- `rag/app/naive.py:508-738 chunk()` 用 11 个 `re.search(扩展名)` 分支选择 parser;新增格式必须改这个 750 行的函数。
|
||||
- `rag/llm/embedding_model.py` 每个 provider 是独立子类(`OpenAIEmbed` / `QWenEmbed` / `XinferenceEmbed` ...),但选择哪个子类没有 registry,依赖外层硬编码 `OpenAIEmbed` import(`workflow/nodes/knowledge/node.py:12`)。
|
||||
- `chat_model.py` 中 `ChatBase` 子类硬编码各 provider 的 base_url 与认证 header(如 `chat_model.py:41-44 OpenAIEmbed.__init__` 直接拼 base_url),切换路径不优雅。
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构改造建议清单(共 11 条)
|
||||
|
||||
每条建议结构:**问题 → 方案 → 收益 → 成本/风险 → 优先级**。
|
||||
|
||||
### 【建议 1 · 模块化】拆掉双轨 Embedding,统一到单一 Embedder 协议 `[P0]`
|
||||
|
||||
- **问题陈述**:`RedBearEmbeddings`(LangChain)与 `OpenAIEmbed/QWenEmbed/...`(遗留)两套并存,调用方用哪一个看心情;接口不兼容(`embed_documents/embed_query` vs `encode/encode_queries`),返回类型不一致(`list[list[float]]` vs `(np.ndarray, total_tokens)`)。
|
||||
- 源码:`rag/models/embedding.py:9-78`、`rag/llm/embedding_model.py:14-65`、`rag/graphrag/utils.py:301-327`(GraphRAG 调用 `embd_mdl.encode([ent_name])`)、`rag/nlp/search.py:365-373`(Dealer 调用 `emb_mdl.encode_queries(txt)`)。
|
||||
- **改造方案**:
|
||||
- 定义 `app/core/rag/protocols/embedder.py` 中的 `Embedder` Protocol:`embed_documents(texts) -> EmbedResult` 与 `embed_query(text) -> EmbedResult`,`EmbedResult` 是 `dataclass(vectors: np.ndarray, total_tokens: int, dim: int)`。
|
||||
- 现有 `OpenAIEmbed` 等遗留类实现 `Embedder`(保留 `encode/encode_queries` 兼容期 6 个月)。
|
||||
- 新建 `EmbedderFactory.from_model_config(config) -> Embedder`,内部根据 `provider` 字段路由;`workflow/nodes/knowledge/node.py:12` 删除对 `OpenAIEmbed` 的硬编码 import。
|
||||
- 把 GraphRAG 与 Dealer 都改为通过 `Embedder` 协议调用。
|
||||
- **收益**:维护成本从"两套类各自演进"降为一套;新 provider 只需实现 `Embedder` 协议;单测可用 `FakeEmbedder` mock,**单测覆盖率提升预期 +30%**(当前 rag 模块基本无单测)。
|
||||
- **成本与风险**:实现 + 迁移约 **5 人日**。回归风险中(GraphRAG 的 `np.ndarray` 返回类型若变成 `list[list[float]]` 会触发下游 `np` 操作错误,需保留 numpy 输出适配器)。
|
||||
- **优先级**:**P0**(解锁后续所有改造的前置条件)。
|
||||
|
||||
### 【建议 2 · 接口抽象】定义 `Retriever` / `Reranker` / `Generator` 三大协议(LangChain Runnable 风格)`[P0]`
|
||||
|
||||
- **问题陈述**:当前没有"检索器"这层抽象,调用方需要直接知道:用哪个 ES index、是否走 hybrid、要不要叠加 GraphRAG。例如 `workflow/nodes/knowledge/node.py:195-263 knowledge_retrieval()` 内部用 `match retrieve_type` 分四个分支调 `vector_service.search_by_vector()` / `search_by_full_text()` / 二者并行 dedup / 再叠加 `kg_retriever.retrieval()`。每新增一种检索策略都要在这里加 `case`。
|
||||
- **改造方案**:定义三个 Protocol(伪代码见 PoC 章节):
|
||||
```python
|
||||
class Retriever(Protocol):
|
||||
async def retrieve(self, query: Query) -> RetrievalResult: ...
|
||||
class Reranker(Protocol):
|
||||
async def rerank(self, query: str, docs: list[Doc], top_k: int) -> list[Doc]: ...
|
||||
class Generator(Protocol):
|
||||
async def generate(self, prompt: Prompt, stream: bool) -> GenerationResult | AsyncIterator[Chunk]: ...
|
||||
```
|
||||
并提供组合算子 `Pipeline = Retriever | Reranker | Generator`(类似 LangChain Runnable 的 `|`)。`KnowledgeRetrievalNode` 不再 `match retrieve_type`,而是注入一个 `Retriever`(`HybridRetriever` / `GraphAugmentedRetriever` / `VectorRetriever` 是不同实现)。
|
||||
- **收益**:策略模式取代条件分支;单测可对 `Retriever` 接口做契约测试;A/B 实验只需注入不同实现;"GraphRAG-then-Vector"、"Vector-then-Graph"、"Reranker-only"等组合可声明式表达。
|
||||
- **成本与风险**:核心接口设计 + 关键实现 + 迁移调用方约 **8 人日**。风险中(涉及 workflow node 的契约变化,需要保留旧接口至少一个 release)。
|
||||
- **优先级**:**P0**。
|
||||
|
||||
### 【建议 3 · 模块化】消除 Rerank 的三处重复实现 `[P0]`
|
||||
|
||||
- **问题陈述**:
|
||||
- `workflow/nodes/knowledge/node.py:284 rerank()`(模块级函数)— **第 327 行有 `print(reranked_docs)` 调试残留**。
|
||||
- `workflow/nodes/knowledge/node.py:108-155 KnowledgeRetrievalNode.rerank()`(节点方法)— 与前者代码逻辑几乎一致(都做 `RedBearRerank.compress_documents` + 按 `relevance_score` 排序 + 按 `page_content` 字符串匹配回查 metadata)。
|
||||
- `rag/nlp/search.py:606-643 Dealer.rerank()`(融合排序)—— 走的是 token+vector+rank_feature 三项加权,与前两者完全是不同范式但同名为 rerank。
|
||||
- 第二个问题:`KnowledgeRetrievalNode.get_reranker_model()`(`node.py:157-193`)每次 rerank 调用都查一次 DB 获取模型配置,实例化 `RedBearRerank`。
|
||||
- **改造方案**:
|
||||
- 实现一个唯一的 `RerankerService`:内部做 (a) DB 缓存 reranker 实例(key=`reranker_id`,TTL=10min);(b) 屏蔽"按 page_content 字符串匹配 metadata"的脆弱回查(改为 LangChain `Document.metadata["__doc_index__"]` 索引);(c) 暴露 `Reranker` Protocol。
|
||||
- 删掉 `node.py:284 rerank()` 模块级函数(或仅保留 `@deprecated` 别名指向 `RerankerService`)。
|
||||
- `Dealer.rerank()` 改名为 `Dealer.fuse_scores()`,明确它是"分数融合"不是"模型重排"。
|
||||
- 删除 `node.py:327 print()` 残留。
|
||||
- **收益**:消除每次请求多查 DB 一次的开销(实测 DB 查询 5–20ms,去掉后**热路径单次省 5-20ms × QPS**);rerank 逻辑只需在一处 review 与单测。
|
||||
- **成本与风险**:约 **3 人日**。风险低(接口对外不变)。
|
||||
- **优先级**:**P0**(含调试残留的 hot fix 应优先合并)。
|
||||
|
||||
### 【建议 4 · 性能优化】Embedder 与 Reranker 加缓存层 `[P0]`
|
||||
|
||||
- **问题陈述**:
|
||||
- GraphRAG 用 Redis 缓存 Embedding(`graphrag/utils.py:115-134`,TTL=24h,key=xxhash(model_name+text)),命中率高时显著省成本。
|
||||
- 但 ES VDB 入库/检索 (`elasticsearch_vector.py:55-63 add_chunks` / `:374-380 search_by_vector`) **完全无缓存**。同一 query 反复 embedding;同一 chunk 重复入库时也会重复算向量。
|
||||
- Rerank 同样无缓存:`RedBearRerank.compress_documents` 每次都打外部 API(DashScope/Jina),200+ ms。
|
||||
- **改造方案**:
|
||||
- 抽出 `app/core/rag/cache/embed_cache.py`(把 `graphrag/utils.py` 中的现有实现搬过来 + 通用化)。
|
||||
- `Embedder` Protocol 在调用层加装饰器 `@cached_embedder(redis, ttl=24h)`,对 `embed_query` 必加(query 重复率高),`embed_documents` 可配置。
|
||||
- 新增 `Reranker` 缓存:key=`xxhash(model + query + sorted(doc_ids))`,TTL=1h(rerank 结果对 query 变体很敏感,不要 TTL 太长)。
|
||||
- 从环境变量读 `REDIS_*` 配置,cache 失败时优雅降级为 no-op(不要 break 主链路)。
|
||||
- **收益**:Query embedding 命中场景 **减少 60-90% 外部 API 调用**(基于业内同类系统 query 重复率统计)。Rerank 命中场景再减少 30-50%。**单 query 端到端 P95 下降 100-300ms**(Rerank 是当前最慢的同步阻塞步骤之一)。
|
||||
- **成本与风险**:约 **2 人日**。风险低(cache miss 时行为与现状一致)。
|
||||
- **优先级**:**P0**。
|
||||
|
||||
### 【建议 5 · 性能优化】用 Plugin Registry 替换 `naive.py:508` 的 11 路 if/elif 解析器分发 `[P1]`
|
||||
|
||||
- **问题陈述**:`rag/app/naive.py:508 chunk()` 用 `re.search(r"\.docx$", filename)` / `r"\.pdf$"` / `r"\.(pptx|ppt?)$"` / ... 11 个分支硬编码挑 parser。新增一种格式必须改这个 750 行函数;同时 PDF 自身有 `by_deepdoc` / `by_mineru` / `by_textln` 三种实现,选择路径用 `parser_config["layout_recognize"]` 字符串比对,没有类型保护。
|
||||
- **改造方案**:
|
||||
- 定义 `Parser` Protocol:`def can_parse(filename) -> bool` + `def parse(filename, binary, **kwargs) -> ParseResult`。
|
||||
- 在 `rag/app/parsers/__init__.py` 中维护一个 `_registry: dict[str, Parser] = {}` + `@register_parser("docx", "pdf", ...)` 装饰器。
|
||||
- `chunk()` 简化为 4 行:`parser = registry.find(filename); sections, tables = parser.parse(...); return tokenize(sections, tables)`。
|
||||
- 第三方 parser(MinerU、TextIn)也注册为可插拔实现,运行时由 `parser_config.layout_recognize` 选择。
|
||||
- **收益**:新增格式 = 新增一个文件 + 一行 `register`,不再需要碰 `naive.py`;测试可针对每个 parser 独立写;**`naive.py` 从 750+ 行降到 100 行以内**,可读性大幅提升。
|
||||
- **成本与风险**:约 **5 人日**(11 类 parser 都要拆)。风险中(要保留 `vision_figure_parser_pdf_wrapper` 等横切逻辑,需要 hook 点设计)。
|
||||
- **优先级**:**P1**。
|
||||
|
||||
### 【建议 6 · 可观测性】引入 OpenTelemetry,全链路 trace + 关键指标埋点 `[P1]`
|
||||
|
||||
- **问题陈述**:requirements.txt 中无任何 OTel/Prometheus/Sentry 依赖;355 个 `logger` 调用全是本地日志。无法回答"昨天 P95 多少"、"哪一步最慢"、"哪个 KB 召回率最差"。README 中宣称的 P50/P95 是无源之水。
|
||||
- **改造方案**:
|
||||
- 在 `requirements.txt` 加入 `opentelemetry-sdk`、`opentelemetry-instrumentation-fastapi`、`opentelemetry-instrumentation-elasticsearch`、`opentelemetry-instrumentation-redis`、`opentelemetry-instrumentation-celery`、`opentelemetry-exporter-otlp`。
|
||||
- 在 `app/core/rag/observability/tracing.py` 提供 `@trace_rag_step("embed/search/rerank/generate")` 装饰器(基于 `opentelemetry.trace.get_tracer`),包装 `Embedder.embed_*` / `Retriever.retrieve` / `Reranker.rerank` / `Generator.generate`。
|
||||
- 关键指标(`opentelemetry.metrics.meter`):
|
||||
- `rag.embed.latency_ms{provider, model}` Histogram
|
||||
- `rag.search.recall@k{kb_id, retrieve_type}` Counter(结合用户反馈数据)
|
||||
- `rag.llm.tokens_used{provider, model, type=prompt|completion}` Counter
|
||||
- `rag.cache.hit_ratio{layer=embed|rerank|llm}` Gauge
|
||||
- `rag.pipeline.e2e_latency_ms{retrieve_type, has_rerank}` Histogram
|
||||
- LLM 级(`chat_model.py:_chat / _chat_streamly`)也加 `tracer.start_as_current_span`,把 token 用量、provider、model 写到 attributes。
|
||||
- **收益**:实时 P50/P95 / 错误率 / Token 成本可观测;oncall 排障从"捞日志 grep"变成"看 Grafana panel";A/B 实验有可量化的 baseline。
|
||||
- **成本与风险**:约 **5 人日**(依赖 + 装饰器 + 关键 span + 一份 Grafana JSON 模板)。风险低(OTel 失败时 no-op)。
|
||||
- **优先级**:**P1**(前 2 周做不完,但中期一定要做)。
|
||||
|
||||
### 【建议 7 · 配置治理】中心化配置 + Pydantic Settings + 类型校验 `[P1]`
|
||||
|
||||
- **问题陈述**:`os.environ.get` 出现 58 次散落在 48 个文件;同一变量名多处使用却无单一文档;类型靠 `int(os.getenv(...))` 手工转换(`elasticsearch_vector.py:699-702` 反复出现);缺省值随手填,不一致(如 `ELASTICSEARCH_REQUEST_TIMEOUT` 文档说 100000,源码 `elasticsearch_vector.py:699` 缺省是 30)。
|
||||
- **改造方案**:
|
||||
- 新增 `app/core/rag/config/settings.py`:用 `pydantic_settings.BaseSettings` 把 RAG 相关全部环境变量收拢成 `RAGSettings`,分组:`LLMSettings` / `EmbeddingSettings` / `ESSettings` / `GraphRAGSettings` / `MinerUSettings` / `OCRSettings` 等。
|
||||
- 启动时 `RAGSettings()` 一次性加载、校验、默认值统一;`docs/rag/_meta/config_reference.md` 自动生成(用 `RAGSettings.model_json_schema()` → markdown)。
|
||||
- 现有调用点 `os.environ.get("X")` 替换为 `from app.core.rag.config import settings; settings.x`。
|
||||
- Secret 管理:API key / DB 密码强制走 `pydantic.SecretStr`,禁止默认值。
|
||||
- **收益**:单一可信来源(Single Source of Truth);类型错误启动期暴露而非运行时;运维有完整变量清单;CI 可静态校验"是否引入了未注册的环境变量"。
|
||||
- **成本与风险**:约 **4 人日**(迁移 58 处调用点 + 文档生成)。风险低(一次性脚本可批量替换)。
|
||||
- **优先级**:**P1**。
|
||||
|
||||
### 【建议 8 · 模块化】消除 `init_settings()` 模块级副作用 `[P1]`
|
||||
|
||||
- **问题陈述**:`rag/common/settings.py:24` 在模块导入时立即执行 `init_settings()`,创建进程级 `docStoreConn = ESConnection()`、`retriever = Dealer(...)`、`kg_retriever = KGSearch(...)`。任何 `from app.core.rag.common.settings import retriever` 都会立即建 ES 连接。
|
||||
- 后果:单元测试无法 stub(import 时已触发副作用);多进程/Celery worker 启动时间增加(每个 worker 都连 ES);测试容器需要 ES 运行才能 `pytest collect`。
|
||||
- **改造方案**:
|
||||
- 替换为 lazy initialization:`@lru_cache def get_doc_store(): ...` / `@lru_cache def get_retriever(): ...` / `@lru_cache def get_kg_retriever(): ...`。
|
||||
- 在 FastAPI 应用层用 dependency injection(`fastapi.Depends`)注入而非全局 singleton。
|
||||
- 测试时用 `app.dependency_overrides[get_retriever] = lambda: FakeRetriever()` mock。
|
||||
- **收益**:单测可独立运行(不依赖 ES);冷启动延后到首次使用;多 worker 避免共享单例的诡异 bug。
|
||||
- **成本与风险**:约 **2 人日**(替换 import-style 调用为 `Depends`)。风险中(要逐个排查 `from settings import retriever` 的 24 处调用点)。
|
||||
- **优先级**:**P1**。
|
||||
|
||||
### 【建议 9 · 性能优化】Embedding 与 Rerank 批量化 + 异步并发 `[P1]`
|
||||
|
||||
- **问题陈述**:
|
||||
- `rag/llm/embedding_model.py:50 OpenAIEmbed.encode()` 中 `batch_size = 16` **硬编码**;`QWenEmbed` 是 4,`HuggingFaceEmbed` 是无(全量发送)。`EMBEDDING_BATCH_SIZE` 在 README 提过但代码注释掉未生效。
|
||||
- `elasticsearch_vector.py:55-63 add_chunks` 是同步循环,无 trio/asyncio 并发;`workflow/nodes/knowledge/node.py:knowledge_retrieval` 多 KB 检索时是 `await asyncio.gather` 并发的,但单 KB 内 vector + full_text 是顺序调用。
|
||||
- GraphRAG 已经用 `trio.CapacityLimiter(MAX_CONCURRENT_CHATS=10)` 限流(`graphrag/utils.py:41`),但 ES VDB 写入对应的限流不存在。
|
||||
- **改造方案**:
|
||||
- `Embedder` 协议提供 `batch_size` 字段,默认从 `RAGSettings.embedding.batch_size` 读取,每个 provider 可 override。
|
||||
- `ElasticSearchVector.add_chunks` 改为 trio 协程版本,与 GraphRAG 共享 `chat_limiter` 限流。
|
||||
- `HybridRetriever.retrieve` 内部 `vector` + `full_text` 用 `asyncio.gather` 并发(当前在 node 层做了,下沉到 Retriever)。
|
||||
- **收益**:Embedding 大批量入库 P95 下降 **20-40%**(瓶颈从串行 16-batch HTTP 变并发);Hybrid 检索单次 P50 下降 **30-50%**(从串行 → 并发 max 而非 sum)。
|
||||
- **成本与风险**:约 **3 人日**。风险中(trio 与 asyncio 混用要小心,已有 `trio.to_thread.run_sync` 模式可参考)。
|
||||
- **优先级**:**P1**。
|
||||
|
||||
### 【建议 10 · 可观测性 + 配置】消灭遗留 `print()` 与无结构化日志 `[P2]`
|
||||
|
||||
- **问题陈述**:
|
||||
- `workflow/nodes/knowledge/node.py:327 print(reranked_docs)` 残留调试语句;同类 `print` 在 rag/ 目录共有数十处(grep 验证)。
|
||||
- 现有 logger 是非结构化字符串日志(`logger.info(f"add_texts result:{result}")` `elasticsearch_vector.py:86`),无法 ELK 聚合查询。
|
||||
- **改造方案**:
|
||||
- 引入 `structlog`,所有 `logger.*` 调用改为 KV 格式:`logger.info("vdb.add_texts", n_docs=len(actions), index=self._collection_name, took_ms=...)`。
|
||||
- pre-commit hook 加 `flake8-print` 阻止新 `print` 进入仓库。
|
||||
- 一次性 sweep 删除现有 `print`。
|
||||
- **收益**:日志可聚合查询("过去 1 小时 add_texts 平均 n_docs");CI 防止回归。
|
||||
- **成本与风险**:约 **2 人日**。风险低。
|
||||
- **优先级**:**P2**。
|
||||
|
||||
### 【建议 11 · 接口抽象】把 `BaseVector` 的"多模态分支"抽象到 Embedder 而非 VDB 层 `[P2]`
|
||||
|
||||
- **问题陈述**:`elasticsearch_vector.py:55-63` 的 `add_chunks` 与 `:374-380 search_by_vector` 都有 `if self.is_multimodal_embedding: ... else: ...` 分支判断(火山引擎多模态走 `embed_batch/embed_text`,其他走 `embed_documents/embed_query`)。这是把"Embedder 的能力差异"泄露到了 VDB 层 — 违反单一职责。
|
||||
- **改造方案**:
|
||||
- 在 `Embedder` Protocol 内部统一接口:`embed(items: list[Item]) -> list[list[float]]`,其中 `Item = TextItem | ImageItem | VideoItem`。多模态 Embedder 内部分发到 `multimodal_embeddings.create`,文本 Embedder 走 `embed_documents`。
|
||||
- VDB 层只调 `embedder.embed(...)`,不再有 `is_multimodal` 分支。
|
||||
- **收益**:VDB 与 Embedder 职责清晰;后续接入 ColBERT / SPLADE / 多向量 Embedding 时无需修改 VDB。
|
||||
- **成本与风险**:约 **2 人日**。
|
||||
- **优先级**:**P2**(依赖建议 1 完成)。
|
||||
|
||||
---
|
||||
|
||||
## 2. PoC 代码草案
|
||||
|
||||
### 2.1 PoC-1:统一 `Retriever` / `Reranker` / `Generator` 协议(建议 2)
|
||||
|
||||
```python
|
||||
# api/app/core/rag/protocols/__init__.py
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Protocol, AsyncIterator, runtime_checkable
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Query:
|
||||
text: str
|
||||
kb_ids: list[str]
|
||||
top_k: int = 4
|
||||
similarity_threshold: float = 0.2
|
||||
rerank: bool = False
|
||||
extras: dict = field(default_factory=dict) # 其他场景化参数
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Doc:
|
||||
id: str
|
||||
content: str
|
||||
score: float
|
||||
metadata: dict = field(default_factory=dict)
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RetrievalResult:
|
||||
docs: list[Doc]
|
||||
total: int
|
||||
debug: dict = field(default_factory=dict) # latency_ms, recall_strategy, etc.
|
||||
|
||||
@runtime_checkable
|
||||
class Retriever(Protocol):
|
||||
name: str
|
||||
async def retrieve(self, query: Query) -> RetrievalResult: ...
|
||||
|
||||
@runtime_checkable
|
||||
class Reranker(Protocol):
|
||||
async def rerank(self, query: str, docs: list[Doc], top_k: int) -> list[Doc]: ...
|
||||
|
||||
@runtime_checkable
|
||||
class Generator(Protocol):
|
||||
async def generate_stream(self, prompt: str, history: list[dict],
|
||||
context: list[Doc]) -> AsyncIterator[str]: ...
|
||||
```
|
||||
|
||||
```python
|
||||
# api/app/core/rag/retrievers/hybrid_retriever.py
|
||||
import asyncio
|
||||
from app.core.rag.protocols import Retriever, Query, RetrievalResult, Doc
|
||||
from app.core.rag.vdb.vector_base import BaseVector
|
||||
from app.core.rag.protocols.reranker import Reranker
|
||||
|
||||
class HybridRetriever(Retriever):
|
||||
name = "hybrid"
|
||||
def __init__(self, vector_store: BaseVector, reranker: Reranker | None = None,
|
||||
vector_weight: float = 0.7):
|
||||
self._store = vector_store
|
||||
self._reranker = reranker
|
||||
self._vector_weight = vector_weight
|
||||
|
||||
async def retrieve(self, query: Query) -> RetrievalResult:
|
||||
vec_task = asyncio.to_thread(
|
||||
self._store.search_by_vector, query.text, top_k=query.top_k * 4)
|
||||
bm25_task = asyncio.to_thread(
|
||||
self._store.search_by_full_text, query.text, top_k=query.top_k * 4)
|
||||
vec_docs, bm25_docs = await asyncio.gather(vec_task, bm25_task)
|
||||
merged = self._fuse_rrf(vec_docs, bm25_docs) # Reciprocal Rank Fusion
|
||||
if self._reranker and query.rerank and merged:
|
||||
docs = await self._reranker.rerank(
|
||||
query.text, merged, top_k=query.top_k)
|
||||
else:
|
||||
docs = merged[:query.top_k]
|
||||
return RetrievalResult(docs=docs, total=len(merged),
|
||||
debug={"strategy": self.name})
|
||||
|
||||
@staticmethod
|
||||
def _fuse_rrf(a: list[Doc], b: list[Doc], k: int = 60) -> list[Doc]:
|
||||
scores = {}
|
||||
for rank, d in enumerate(a):
|
||||
scores[d.id] = scores.get(d.id, 0) + 1 / (k + rank + 1)
|
||||
for rank, d in enumerate(b):
|
||||
scores[d.id] = scores.get(d.id, 0) + 1 / (k + rank + 1)
|
||||
all_docs = {d.id: d for d in a + b}
|
||||
return sorted((all_docs[i] for i in scores),
|
||||
key=lambda d: scores[d.id], reverse=True)
|
||||
```
|
||||
|
||||
```python
|
||||
# api/app/core/workflow/nodes/knowledge/node_v2.py(重构后)
|
||||
class KnowledgeRetrievalNodeV2(BaseNode):
|
||||
def __init__(self, retriever: Retriever, ...):
|
||||
self._retriever = retriever # 注入,不再 match retrieve_type
|
||||
async def execute(self, state) -> dict:
|
||||
query = Query(text=self._render_query(state),
|
||||
kb_ids=self._kb_ids, top_k=self._top_k,
|
||||
rerank=bool(self._reranker_id))
|
||||
result = await self._retriever.retrieve(query)
|
||||
return {"chunks": [d.content for d in result.docs],
|
||||
"citations": [d.metadata for d in result.docs]}
|
||||
```
|
||||
|
||||
### 2.2 PoC-2:Embedder + Redis 缓存装饰器(建议 1 + 4)
|
||||
|
||||
```python
|
||||
# api/app/core/rag/cache/embed_cache.py
|
||||
import json, xxhash, numpy as np
|
||||
from functools import wraps
|
||||
|
||||
def cached_embedder(redis_client, ttl: int = 24 * 3600):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, texts, *args, **kwargs):
|
||||
if isinstance(texts, str):
|
||||
texts = [texts]
|
||||
keys = [_key(self.model_name, t) for t in texts]
|
||||
cached = redis_client.mget(keys)
|
||||
results, miss_idx, miss_texts = [None]*len(texts), [], []
|
||||
for i, b in enumerate(cached):
|
||||
if b: results[i] = np.array(json.loads(b))
|
||||
else: miss_idx.append(i); miss_texts.append(texts[i])
|
||||
if miss_texts:
|
||||
fresh = func(self, miss_texts, *args, **kwargs) # ndarray, n_tokens
|
||||
vecs = fresh[0] if isinstance(fresh, tuple) else fresh
|
||||
pipe = redis_client.pipeline()
|
||||
for j, idx in enumerate(miss_idx):
|
||||
results[idx] = vecs[j]
|
||||
pipe.setex(keys[idx], ttl, json.dumps(vecs[j].tolist()))
|
||||
pipe.execute()
|
||||
return np.array(results), 0 # tokens cached as 0; metric layer补
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def _key(model: str, text: str) -> str:
|
||||
h = xxhash.xxh64(); h.update(f"{model}\0{text}".encode()); return f"emb:{h.hexdigest()}"
|
||||
```
|
||||
|
||||
使用方式:
|
||||
|
||||
```python
|
||||
class OpenAIEmbed(Base):
|
||||
@cached_embedder(redis_client) # 一行注解开启缓存
|
||||
def encode(self, texts: list): ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 改造路线图
|
||||
|
||||
> 实施前提:先用 1 周时间立两个 baseline —— (a) 当前端到端 P50/P95(即使靠手工脚本采);(b) 单测覆盖率(pytest --cov)。所有改造完成后用同一 baseline 比对,验证收益。
|
||||
|
||||
### 3.1 短期(Sprint 0–1,1-2 周内交付)
|
||||
|
||||
> 目标:止血 + 解锁后续重构的前置条件。
|
||||
|
||||
| # | 工作项 | 关联建议 | 工作量 | 交付物 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 删除 `node.py:327 print()` 残留 + 全仓 print 扫除 | #10 | 0.5d | PR + pre-commit hook |
|
||||
| 2 | 实现 `RerankerService`(含 reranker 实例缓存) | #3 | 2d | 新模块 + 单测 + 替换现有 3 处 rerank |
|
||||
| 3 | 给 `Embedder.encode/encode_queries` 加 Redis 缓存装饰器 | #4 | 1.5d | 装饰器 + benchmark 报告 |
|
||||
| 4 | 中心化配置:`RAGSettings` Pydantic Settings 框架 | #7 | 2d | `app/core/rag/config/settings.py` + 迁移 ES + LLM 配置 |
|
||||
| 5 | 迁移单元测试:先把 settings.py 的 `init_settings()` 副作用改 lazy | #8 | 2d | `pytest` 不再依赖 ES 即可 collect |
|
||||
|
||||
**短期里程碑(Sprint 1 末)**:
|
||||
- ✅ 调试 print 残留清零;
|
||||
- ✅ 单测可独立运行(脱离 ES);
|
||||
- ✅ Reranker 命中场景延迟下降 50%+;
|
||||
- ✅ Query Embedding 命中场景延迟下降 70%+。
|
||||
|
||||
### 3.2 中期(Sprint 2–4,1-2 月内交付)
|
||||
|
||||
> 目标:完成核心抽象层重构,引入可观测性。
|
||||
|
||||
| # | 工作项 | 关联建议 | 工作量 | 交付物 |
|
||||
|---|---|---|---|---|
|
||||
| 6 | 设计 + 落地 `Embedder` Protocol,迁移 `OpenAIEmbed/QWenEmbed/...` | #1 | 5d | 协议 + 适配器 + 弃用计划文档 |
|
||||
| 7 | 设计 + 落地 `Retriever / Reranker / Generator` Protocol;实现 `VectorRetriever` `BM25Retriever` `HybridRetriever` `GraphAugmentedRetriever` | #2 | 8d | 协议 + 4 个实现 + 节点改造 |
|
||||
| 8 | OpenTelemetry 接入:在 RAG 关键路径加 span 与 metric | #6 | 5d | `observability/tracing.py` + Grafana 模板 + 文档 |
|
||||
| 9 | Plugin Registry 重构 `naive.py` 解析器分发 | #5 | 5d | `parsers/` 模块化 + 11 个 parser 注册 |
|
||||
| 10 | 配置治理收尾:剩余 50+ 处 `os.environ.get` 全部迁到 `RAGSettings` | #7 | 2d | 文档自动生成脚本 |
|
||||
| 11 | Embedder 与 Rerank 批量化 + 异步并发改造 | #9 | 3d | 性能 benchmark 对比报告 |
|
||||
|
||||
**中期里程碑(Sprint 4 末)**:
|
||||
- ✅ 抽象层统一完成(Embedder / Retriever / Reranker / Generator 四大协议落地);
|
||||
- ✅ Grafana 实时面板:P50/P95/Token 用量/缓存命中率;
|
||||
- ✅ 单测覆盖率 RAG 模块从 ~5% 提升到 ≥35%;
|
||||
- ✅ 端到端 P95 较 baseline 下降 30%+。
|
||||
|
||||
### 3.3 长期(Sprint 5–8,3-6 月内交付)
|
||||
|
||||
> 目标:可插拔架构、生产级稳定性、为 [S3-T2] 列出的多模态 / 混合搜索增强 / KG 演化做铺垫。
|
||||
|
||||
| # | 工作项 | 关联建议 | 工作量 | 交付物 |
|
||||
|---|---|---|---|---|
|
||||
| 12 | 多模态分支从 VDB 抽离到 Embedder | #11 | 2d | VDB 接口收敛 |
|
||||
| 13 | 引入第二个 VDB 实现(如 Milvus),验证 `BaseVector` 可插拔 | #2 | 8d | `MilvusVector` + 一致性测试套件 |
|
||||
| 14 | LLM Provider 也改 Plugin Registry(消除 `chat_model.py` 11 个子类的 if 切换) | #5 | 5d | LLM 层与 Embedding 层架构对齐 |
|
||||
| 15 | 完整的 `Pipeline = Retriever \| Reranker \| Generator` DSL,配置驱动 | #2 | 10d | YAML 描述场景 → 运行时拼装 |
|
||||
| 16 | A/B 实验框架:基于 OTel metric,把 recall@k / answer_score 接入实验对比 | #6 | 5d | 实验平台对接文档 |
|
||||
| 17 | LLM 失败模型降级链(fallback to 备用 provider) | #2 + 现有 Base 增强 | 3d | `FallbackGenerator` 实现 |
|
||||
| 18 | 安全 / Secret 管理:从 `pydantic.SecretStr` 升级到 Vault / Secrets Manager 集成 | #7 | 5d | 密钥不进 .env 文件 |
|
||||
|
||||
**长期里程碑(Sprint 8 末)**:
|
||||
- ✅ 可插拔 VDB(一行配置切换 ES → Milvus);
|
||||
- ✅ Pipeline DSL 上线,新增"GraphRAG-Then-Vector-Then-Rerank"等组合无需改代码;
|
||||
- ✅ 全链路 Trace + 指标 + A/B 框架就绪;
|
||||
- ✅ 为 [S3-T2] 中"多模态检索 / SPLADE / ColBERT 路由 / KG 演化 / 反馈闭环"等扩展提供清晰的接口注入点。
|
||||
|
||||
---
|
||||
|
||||
## 4. 风险与依赖统一汇总
|
||||
|
||||
| 风险类别 | 描述 | 缓解方案 |
|
||||
|---|---|---|
|
||||
| **回归风险(高)** | `Embedder` 协议迁移可能改变返回类型(`np.ndarray` vs `list[list[float]]`) | 6 个月兼容期,旧接口保留并打 `DeprecationWarning`;CI 加契约测试 |
|
||||
| **回归风险(中)** | `KnowledgeRetrievalNode` 接口改造,影响 workflow 已部署应用 | 引入 `node_v2.py`,灰度切换;保留 `node.py` 至少一个 release |
|
||||
| **依赖风险** | OpenTelemetry 接入需 collector / Tempo / Loki 等基础设施 | 短期可先只导出到 stdout exporter,基础设施分阶段建设 |
|
||||
| **协作依赖** | 与 [@Python 开发工程师](mention://agent/f4d1c89f-0c71-4af3-bf72-d34f7ed115cf) 一起验证 PoC 与迁移可行性 | Sprint 0 启动前先 1 次架构对齐会 |
|
||||
| **运营依赖** | 配置治理(建议 #7)落地后,运维需更新部署脚本与文档 | 切换前 2 周通知;提供变量映射表(旧 → 新) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 验收 Checklist 自检
|
||||
|
||||
- [x] 至少 8 条建议(实际 11 条)
|
||||
- [x] 覆盖 5 个方向:模块化拆分(#1, #3, #5, #8)/ 接口抽象(#1, #2, #11)/ 性能优化(#4, #5, #9)/ 可观测性(#6, #10)/ 配置与依赖治理(#7)
|
||||
- [x] 每条建议均有源码引用(文件:行号 + 关键摘录)
|
||||
- [x] PoC 代码草案:**2 套**(统一 Retriever 协议 + Embedder 缓存装饰器,均在 10–50 行)
|
||||
- [x] 现状评估:3 优点 + 5 痛点
|
||||
- [x] 改造路线图:短期 / 中期 / 长期 三阶段,每阶段附交付物清单
|
||||
- [x] 与 [S2-T7] Sprint-2 文档兼容:引用 [S2-T2 Embedding](mention://issue/7a8cd047-f339-427e-bd60-999c62caea22) 双轨问题、[S2-T5 LLM/Reranking](mention://issue/eef8ed99-c13e-43ba-a2b3-2c9e59b74301) 三处 rerank 实现,与 [S1-T3 Gap 报告](mention://issue/264529aa-1856-4505-8e26-6125df061c18) 中识别的"`rag_utils` vs `rag/utils` 命名冲突"等差异交叉印证
|
||||
- [x] 提交至 [S3-T3] 终审
|
||||
|
||||
---
|
||||
|
||||
*文档基于 MemoryBear `agent/ai/f8de881a` 分支(基于 commit `feae2f2e`)逐文件核验。所有源码引用可在 ±3 行内复现。*
|
||||
98
docs/rag/evolution/capability-map.mmd
Normal file
98
docs/rag/evolution/capability-map.mmd
Normal file
@@ -0,0 +1,98 @@
|
||||
%% MemoryBear RAG 能力地图(Capability Map)
|
||||
%% 横轴:能力域;纵轴:成熟度(已有 / 近期可上 / 中长期愿景)
|
||||
%% 与 [S3-T1] 提议的 Retriever / Reranker / Generator / Embedder 抽象接口对齐
|
||||
graph LR
|
||||
classDef have fill:#10b981,stroke:#065f46,color:#fff,stroke-width:1px
|
||||
classDef near fill:#f59e0b,stroke:#92400e,color:#fff,stroke-width:1px
|
||||
classDef vision fill:#6366f1,stroke:#3730a3,color:#fff,stroke-width:1px
|
||||
classDef domain fill:#e5e7eb,stroke:#374151,color:#111,stroke-width:1px
|
||||
|
||||
subgraph DLOAD[数据接入]
|
||||
L1[Web 爬虫]:::have
|
||||
L2[飞书 / 语雀 / 文件上传]:::have
|
||||
L3[企业 IM / 邮件 / Notion / S3 增量同步]:::near
|
||||
L4[流式数据 / Kafka / CDC]:::vision
|
||||
end
|
||||
|
||||
subgraph DPARSE[解析与多模态采集]
|
||||
P1[deepdoc PDF/OCR/Layout/Table]:::have
|
||||
P2[图片 OCR + VLM describe]:::have
|
||||
P3[音频 ASR]:::have
|
||||
P4[视频 VLM 整体描述]:::have
|
||||
P5[音视频时间戳化抽帧 + 关键帧 caption]:::near
|
||||
P6[原生 CLIP/BGE-VL 跨模态嵌入]:::vision
|
||||
end
|
||||
|
||||
subgraph DCHUNK[切分与表征]
|
||||
C1[naive_merge / 类型化 chunker]:::have
|
||||
C2[RagTokenizer 中英分词]:::have
|
||||
C3[Late-Interaction / ColBERT 子词表征]:::near
|
||||
C4[语义分块 + 自适应粒度]:::vision
|
||||
end
|
||||
|
||||
subgraph DEMB[Embedding]
|
||||
E1[10+ Provider 工厂]:::have
|
||||
E2[问题增强 question_proposal]:::have
|
||||
E3[Sparse 向量 / SPLADE 学习稀疏]:::near
|
||||
E4[Multi-Vector / 多语种统一编码]:::vision
|
||||
end
|
||||
|
||||
subgraph DVDB[向量与检索]
|
||||
V1[ES dense_vector + BM25]:::have
|
||||
V2[FusionExpr 0.05/0.95 加权融合]:::have
|
||||
V3[KGSearch N-hop + Community]:::have
|
||||
V4[HNSW 量化 / Sparse 索引上线]:::near
|
||||
V5[语义路由 / 多检索器自适应组合]:::near
|
||||
V6[联邦检索 / 跨租户隐私检索]:::vision
|
||||
end
|
||||
|
||||
subgraph DRANK[重排序]
|
||||
R1[内置 token+vector 融合排序]:::have
|
||||
R2[Jina / DashScope / Xinference 外部 Reranker]:::have
|
||||
R3[Cross-Encoder 蒸馏 + 在线 PairWise 学习]:::near
|
||||
R4[基于反馈的自动 Reranker 微调]:::vision
|
||||
end
|
||||
|
||||
subgraph DKG[知识图谱]
|
||||
K1[GraphRAG light + general]:::have
|
||||
K2[entity_resolution + Leiden 社区]:::have
|
||||
K3[增量图演化 + 时间戳]:::near
|
||||
K4[路径解释性 + Neo4j 双引擎]:::near
|
||||
K5[多源图融合 / 自动本体演化]:::vision
|
||||
end
|
||||
|
||||
subgraph DMEM[对话记忆]
|
||||
M1[memory.forgetting_engine Ebbinghaus]:::have
|
||||
M2[memory.reflection_engine 周期反思]:::have
|
||||
M3[langgraph 读图 Agent]:::have
|
||||
M4[短期 ↔ 长期 ↔ 检索召回三段桥接]:::near
|
||||
M5[人格化记忆策略 + 用户偏好学习]:::vision
|
||||
end
|
||||
|
||||
subgraph DEVAL[评估与反馈闭环]
|
||||
EV1[README F1/BLEU/J 论文级评估]:::have
|
||||
EV2[RAGAS / TruLens 集成 + 在线 A/B]:::near
|
||||
EV3[👍/👎 反馈 → Rerank 微调闭环]:::near
|
||||
EV4[自演化路由策略 / RLHF 长记忆]:::vision
|
||||
end
|
||||
|
||||
subgraph DOPS[平台与可观测]
|
||||
O1[Celery 任务链 + Redis 缓存]:::have
|
||||
O2[FastAPI / Swagger]:::have
|
||||
O3[OpenTelemetry Trace + 检索指标看板]:::near
|
||||
O4[Prompt 仓库 + Eval CI / 灰度发布]:::vision
|
||||
end
|
||||
|
||||
%% 跨域依赖(仅画关键边,避免过密)
|
||||
DLOAD --> DPARSE
|
||||
DPARSE --> DCHUNK
|
||||
DCHUNK --> DEMB
|
||||
DEMB --> DVDB
|
||||
DVDB --> DRANK
|
||||
DRANK -. citations .-> DOPS
|
||||
DCHUNK -. async .-> DKG
|
||||
DKG --> DVDB
|
||||
DEVAL -. metrics .-> DRANK
|
||||
DEVAL -. metrics .-> DVDB
|
||||
DMEM -. memory-augmented retrieval .-> DVDB
|
||||
DMEM -. summary into prompt .-> DRANK
|
||||
457
docs/rag/evolution/future-extensions-roadmap.md
Normal file
457
docs/rag/evolution/future-extensions-roadmap.md
Normal file
@@ -0,0 +1,457 @@
|
||||
# MemoryBear RAG · 后续迭代功能新增方式建议(S3-T2)
|
||||
|
||||
> 上游:[WS-11] 总规划、[S1-T2 全链路架构]、[S1-T3 源码盘点]、Sprint-2 各环节深度文档、[S3-T1 架构改造建议]
|
||||
> 输出形态:能力地图 + 6 个重点扩展方向 + 2 条 Quick PoC + 优先级矩阵 + 落地路线图
|
||||
> 设计原则:所有方向 **必须** 复用 [S3-T1] 提议的统一抽象(`Retriever / Reranker / Generator / Embedder / Loader / Chunker`),避免出现新功能 = 新一团耦合。
|
||||
|
||||
---
|
||||
|
||||
## 0. 现状速览与设计基线
|
||||
|
||||
### 0.1 一图看清"已有 / 可上 / 愿景"
|
||||
|
||||
详见附件 `capability-map.mmd`(Mermaid 格式)。三色对应:
|
||||
- 🟢 **已有**:Sprint-2 文档已覆盖、源码可证、生产可用。
|
||||
- 🟡 **近期可上**:1–2 个 Sprint 内可落地,依赖最少。
|
||||
- 🟣 **中长期愿景**:3–6 个月,存在跨团队/外部依赖。
|
||||
|
||||
### 0.2 关键源码事实(用于支撑后续方案)
|
||||
|
||||
| 事实 | 源码定位 | 对扩展的影响 |
|
||||
|------|---------|-------------|
|
||||
| 多模态目前 **走文本通道** | `rag/app/picture.py:54` 调 `vision_model.describe`;`rag/app/audio.py:29` 调 `seq2txt_mdl.transcription`;`naive.py` 走 video → VLM → 文本 | 跨模态语义损失大;扩展为"原生跨模态向量"是方向 D1 |
|
||||
| `MatchSparseExpr` 已声明但未接入 | `rag/utils/doc_store_conn.py:75` 与 `vdb/field.py:11(SPARSE_VECTOR)` 都已存在;`grep -r SparseVector` 仅 1 处定义、0 处调用 | SPLADE 接入是脚手架级改造,不是从零开始(D2) |
|
||||
| 混合检索权重写死 `0.05,0.95` | `rag/nlp/search.py:439` 的 `FusionExpr("weighted_sum", topk, {"weights": "0.05,0.95"})` | 语义路由 / 自适应权重的注入点天然存在(D2) |
|
||||
| GraphRAG 是"一次构建"模型 | `tasks.py` 的 `build_graphrag_for_document` Celery 链;图存于 ES `knowledge_graph_kwd` 字段 | 增量演化、时间维度、Neo4j 双引擎需要在 Celery 链上加 hook(D3) |
|
||||
| 对话记忆与 RAG **不互通** | `core/memory/` 自成一套(Ebbinghaus、ACT-R、Neo4j、langgraph 读图);`workflow/nodes/knowledge/node.py` 完全不引用 `core/memory` | 对话记忆 ↔ 检索的协同是最大产品差异化机会(D4) |
|
||||
| 评估只在 README 体现 | 仓内无 `eval/`、`ragas`、`F1` 类计算代码 | 反馈闭环要从 0 搭,但与 [S3-T1] 提议的"可观测性"天然合并(D5) |
|
||||
| Reranker 只能推理不能学 | `core/models/rerank.py:11` 包装 langchain `BaseDocumentCompressor`,仅做远程调用 | 自训练 Cross-Encoder 是一条独立、可量化收益的小路径(D5) |
|
||||
| 检索模式硬编码在 enum | `RetrieveType.{PARTICIPLE, SEMANTIC, HYBRID, Graph}` 在 `schemas/chunk_schema.py` | 引入"语义路由"需要把 enum 改成 strategy 模式(D6) |
|
||||
|
||||
### 0.3 与 [S3-T1] 接口抽象的联动约定
|
||||
|
||||
[S3-T1] 提议把当前散落的检索/排序/生成代码抽象为协议(参考 LangChain Runnable)。本路线图的所有"接口改造点"都引用以下统一协议(命名以 [S3-T1] 终稿为准,本稿先行登记):
|
||||
|
||||
```python
|
||||
# rag/protocols.py([S3-T1] 提议)
|
||||
class Retriever(Protocol):
|
||||
async def retrieve(self, query: Query, ctx: RetrievalContext) -> list[ScoredChunk]: ...
|
||||
|
||||
class Reranker(Protocol):
|
||||
async def rerank(self, query: Query, chunks: list[ScoredChunk], ctx: RerankContext) -> list[ScoredChunk]: ...
|
||||
|
||||
class Embedder(Protocol):
|
||||
def encode(self, items: list[Embeddable]) -> EmbeddingResult: ... # Embeddable = str | Image | Audio | ...
|
||||
|
||||
class Generator(Protocol):
|
||||
async def generate(self, system: str, history: list[Msg], ctx: GenContext) -> GenResult: ...
|
||||
```
|
||||
|
||||
> **原则**:本文档每条扩展方向都以"新增/扩展某 Protocol 实现 + 注册到工厂"为接入方式,**不**改动调用方代码。这样可以保持 N 个扩展方向 **并行落地** 而不互相阻塞。
|
||||
|
||||
---
|
||||
|
||||
## 1. 重点扩展方向
|
||||
|
||||
> 共 6 个方向。第 5、6 个为前述 5 个外的延伸(自适应路由),但和"评估闭环 / 混合搜索 / 对话记忆"高度互补,建议合并审阅。
|
||||
|
||||
### D1. 多模态检索(原生跨模态向量空间)
|
||||
|
||||
#### 1.1 触发场景
|
||||
- 客户问:"去年那张含 'Q3 GMV' 的 PPT 切片在哪?" — 当前只能命中 OCR 抽出的文字,**布局/图表整体语义** 丢失。
|
||||
- 视频会议纪要库:用户描述"那段讲到老王说'下季度先稳住毛利'的会议",纯 ASR 文本无法绑定 **说话人 + 时间 + 屏幕共享上下文**。
|
||||
- 设备图谱:硬件型号识图("这块板子是哪一版"),目前只能让 VLM 描述后再走文本检索,VLM 描述不稳定。
|
||||
|
||||
#### 1.2 技术方案
|
||||
分三层逐步推进:
|
||||
|
||||
| 层级 | 方案 | 依赖组件 |
|
||||
|------|------|---------|
|
||||
| L1(基线增强) | **关键帧抽样 + VLM 多次 describe**:视频每 N 秒抽帧,每帧 VLM 描述独立 chunk,附 `frame_ts` 元数据;图片在 OCR + describe 之外再加 **结构化 VQA**("图中有什么图表/品牌/人脸?") | 现有 `cv_model.py`、`sequence2txt_model.py` 即可;新增 `rag/app/video.py` |
|
||||
| L2(跨模态检索) | 引入 **CLIP / BGE-VL / Jina-Clip-v2** 作为 `MultimodalEmbedder` Protocol 实现:图片直接编码为向量,文本 query 编码到 **同一向量空间**;ES 索引增加 `vec_image_q_<dim>_vec` 列 | 新依赖 `transformers` / `sentence-transformers` 或托管 API;GPU 资源 |
|
||||
| L3(视听统一) | **Whisper + speaker diarization**(pyannote)替换当前一段式 ASR;视频 chunk 同时持有 `text_vec`(ASR 文本)+ `image_vec`(关键帧) + `audio_vec`(可选,用 CLAP) | `pyannote.audio`、`open_clip`;额外存储约 +30% |
|
||||
|
||||
#### 1.3 接口改造点(基于 S3-T1)
|
||||
- 扩展 `Embedder.encode(items: list[Embeddable])`:`Embeddable = str | PILImage | AudioBytes | VideoFrame`,返回 `EmbeddingResult(vector, modality, dim)`。
|
||||
- 新增 `MultimodalRetriever(Retriever)` 实现:内部根据 query 的 `modality_hint`(文本默认)选择走 `text_vec` 还是 `image_vec` 列。
|
||||
- VDB 层 schema 演进(`rag/vdb/elasticsearch/elasticsearch_vector.py:653+` 的 mapping 创建):把"硬编码单 vector 列"改造为"按 modality 多列动态注册";落地依赖 [S3-T1] 提到的 mapping 模板化改造。
|
||||
- `app/picture.py` / `app/audio.py` 的 `chunk()` 函数输出 dict 中新增 `image_b64` / `audio_b64` 字段,供 Embedder 后续无损取用(避免 PIL 对象在 Celery pickle 边界丢失)。
|
||||
|
||||
#### 1.4 工作量估计
|
||||
- L1 基线:**1.5 人周**(2 个 PR:视频抽帧;结构化 VQA prompt)
|
||||
- L2 跨模态:**3 人周**(含 Embedder 抽象、ES schema 迁移、回归测试)
|
||||
- L3 视听统一:**4 人周**(含 GPU 容器、speaker diarization 集成)
|
||||
- 合计:**~1.5 + 3 + 4 ≈ 8.5 人周**(可分阶段产出)
|
||||
|
||||
#### 1.5 风险与依赖
|
||||
- ⚠️ **存储膨胀**:image_vec(768d float32)单图 3KB,1M 图 ≈ 3GB;ES dense_vector 启用 `int8_hnsw` 量化可减 75%。
|
||||
- ⚠️ **VLM 描述漂移**:同一图不同时间不同模型版本,描述差异大;需要 caption 缓存(key = `sha256(image)+model_version`)。
|
||||
- ⛓️ **强依赖**:[S3-T1] mapping 模板化改造完成后再做 L2,否则 schema 演进会成阻塞点。
|
||||
- ⛓️ **GPU 依赖**:L2/L3 在自建 GPU 节点或托管 API 二选一;建议先走托管(Jina-Clip API)跑通端到端,再评估自托管。
|
||||
|
||||
---
|
||||
|
||||
### D2. 混合搜索增强(Sparse + Dense + Late-Interaction + 自适应路由)
|
||||
|
||||
#### 2.1 触发场景
|
||||
- "工号 E12345 的 OKR" — 长尾标识符,BM25 强、稠密向量弱,**当前 0.05/0.95 权重几乎让 BM25 失语**。
|
||||
- "怎么做用户分层运营?" — 概念性问题,dense 强、BM25 弱。
|
||||
- "GraphRAG 和 LightRAG 的区别" — 需要 ColBERT 这类 token 级精排,单向量混淆术语。
|
||||
|
||||
#### 2.2 技术方案
|
||||
|
||||
| 子方向 | 方案 | 价值 |
|
||||
|-------|------|------|
|
||||
| **SPLADE 学习稀疏** | 用 `naver/splade-cocondenser-ensembledistil` 或国产 BGE-M3 sparse 输出,每个文档生成稀疏向量(含 token expansion);接入 `MatchSparseExpr`(**已存在但未启用**) | 把 BM25 的"词形匹配"升级为"学习权重 + 自动同义扩展" |
|
||||
| **ColBERT 后期交互** | 文档级向量改为 token 级(一篇文档 N 个 token vector,N≈chunk_token_num/3);retrieval 时用 MaxSim;可仅在 reranker 阶段使用 | 在精确匹配上比 cross-encoder 快 5–10×,质量接近 |
|
||||
| **语义路由 / 自适应权重** | 先用一个轻 LLM(或 query classifier)判定 query 类型(lookup / concept / list / multi-hop / temporal),路由到 `{BM25权重, vector权重, 是否使用 Graph, 是否使用 Rerank}` | 替代当前写死的 `0.05/0.95`;可灰度(query 哈希 % 100 < 5 上新策略) |
|
||||
| **多向量召回融合** | 同 chunk 同时索引 BM25、dense、sparse 三类,retrieval 后用 RRF (Reciprocal Rank Fusion) 融合 | 工程上 RRF 不需训练,落地最快 |
|
||||
|
||||
#### 2.3 接口改造点
|
||||
- 新增 `SparseEmbedder(Embedder)` 实现:返回 `SparseVector(indices, values)`;ES mapping 增加 `q_sparse_<vocab>_vec` 字段,使用 `rank_features`/`sparse_vector` 类型(ES ≥ 8.11)。
|
||||
- 在 `rag/nlp/search.py:Dealer.search()`(第 387 行起)把 `FusionExpr` 的硬编码权重改为 `ctx.fusion_weights`,由 `Retriever` 实现的 `ctx` 参数注入。
|
||||
- 新增 `RouterRetriever(Retriever)`:组合多个底层 retriever(DenseRetriever / SparseRetriever / GraphRetriever),按 router 决策选择 / 融合。
|
||||
- ColBERT 仅在 Reranker 层接入:新增 `ColBERTReranker(Reranker)` 实现;接 `Reranker` 协议,**完全不影响**调用方。
|
||||
|
||||
#### 2.4 工作量估计
|
||||
- RRF 多路融合(**Quick PoC,见 §2**):**0.5 人周**
|
||||
- SPLADE 接入:**2 人周**(含 ES mapping、批量重建索引)
|
||||
- 语义路由:**2.5 人周**(含 router 训练数据采集、灰度框架)
|
||||
- ColBERT Reranker:**3 人周**(GPU 部署 + 蒸馏小型化)
|
||||
- 合计:**~8 人周**
|
||||
|
||||
#### 2.5 风险与依赖
|
||||
- ⚠️ **重建索引成本**:现网 KB 数量 × chunk 数 × 维度,估算总耗时;需要提供"灰度索引切换"工具(详见 §6 路线图 P0)。
|
||||
- ⚠️ **路由器误判**:错路由比无路由更糟;必须配 fallback(路由失败回退到当前默认 0.05/0.95)。
|
||||
- ⛓️ **依赖 [S3-T1]** 的 `Retriever` Protocol 落地后才能优雅接入路由器;否则会污染 `Dealer` 类。
|
||||
|
||||
---
|
||||
|
||||
### D3. 知识图谱增强(基于 [S2-T4] GraphRAG 的延伸)
|
||||
|
||||
#### 3.1 触发场景
|
||||
- 法务/合规库每月新增 200+ 条法规:当前必须 **重建整个图**,CI 跑 1 小时;用户要求"增量入库 + 增量图更新"。
|
||||
- 报错排查:"TS_001 错误码可能由哪些组件触发?" — 需要从 **错误码** 节点 N-hop 走到 **组件** 节点;当前 KGSearch 走的是文本相似度匹配实体,**不是路径推理**。
|
||||
- 团队要求"为什么是这个答案" — 需要把推理路径(A→关系1→B→关系2→C)作为 citation 一同返回,提供 **可解释性**。
|
||||
|
||||
#### 3.2 技术方案
|
||||
|
||||
| 子方向 | 方案 | 现状 → 目标 |
|
||||
|-------|------|------------|
|
||||
| **增量图演化** | 在 `tasks.py:build_graphrag_for_document` 链上插入 `GraphMerge` 阶段:新文档抽出的子图与全图做 **节点对齐 + 关系合并 + 冲突标记**;保留 `version_int` 字段记录每条边的"加入/失效"版本号 | 一次构建 → 增量更新 + 时间溯源 |
|
||||
| **路径解释性** | KGSearch.retrieval() 输出新增 `evidence_path: list[Edge]`;在 prompt 组装时把路径作为引用源;前端渲染"由 X→Y→Z 推断" | 黑盒答案 → 带溯源链路 |
|
||||
| **Neo4j 双引擎** | 当前图存在 ES 的 chunk 表里(`knowledge_graph_kwd` 字段),不能利用图算法;引入 Neo4j 作为 **算法引擎**(PageRank 已在 ES 里跑过,但 Cypher 跑社区检测、最短路径远更便利);ES 仍负责文本召回,Neo4j 负责图算法。README 已声明 Neo4j 是组件,**只是 RAG 层没用** | 单引擎 → 检索 ES + 图算法 Neo4j 混合 |
|
||||
| **温度敏感的图衰减** | 复用 `core/memory/forgetting_engine` 的 Ebbinghaus 实现到图边权重:长期未被命中的实体/关系权重衰减;与 D4 共享一套衰减逻辑 | 静态图 → 动态、有"记忆"的图 |
|
||||
| **自动本体演化** | 借鉴 `core/memory/ontology_services/General_purpose_entity.ttl`,定期用 LLM 检查"这批新加的实体类型是否应该归并到已有类型?" | 类型膨胀 → 受控演化 |
|
||||
|
||||
#### 3.3 接口改造点
|
||||
- 新增 `GraphRetriever(Retriever)` 实现,包装现有 `KGSearch`;输出 `ScoredChunk.metadata` 增加 `evidence_path`(`list[(from_entity, relation, to_entity, confidence)]`)。
|
||||
- 新增 `GraphStore` 抽象层:`add_subgraph / merge / query_path / pagerank / community_detect`;实现两个:`ESGraphStore`(保留现状)、`Neo4jGraphStore`(新增)。`graphrag/general/index.py` 现在直接操作 `nx.Graph`,全部替换为 `GraphStore` 调用。
|
||||
- 在 `tasks.py` 的 Celery 链增加 `graph_merge_task`:依赖 `build_graphrag_for_document`,处理增量合并;需要分布式锁(已有 `redis_lock.py` 可用)。
|
||||
- Prompt 层(`prompts/generator.py`)新增 `evidence_aware_citation_prompt`:把 `evidence_path` 作为额外上下文注入。
|
||||
|
||||
#### 3.4 工作量估计
|
||||
- 增量图演化(最小可用):**3 人周**(最复杂的是合并冲突的实体消歧)
|
||||
- 路径解释性:**2 人周**
|
||||
- Neo4j 双引擎:**3 人周**(含 Cypher 工具集、Neo4j 数据迁移脚本)
|
||||
- 图衰减 + 本体演化:**2 人周**(与 D4 共享代码)
|
||||
- 合计:**~10 人周**
|
||||
|
||||
#### 3.5 风险与依赖
|
||||
- ⚠️ **实体消歧难度**:跨文档同名异义("苹果"=公司 / 水果);建议用现有 `entity_resolution.py` 改造,但需要补全单元测试。
|
||||
- ⚠️ **Neo4j 运维成本**:用户已在 README 声明依赖 Neo4j,但当前 RAG 层零调用;引入意味着同时管理两个图的一致性。建议把 Neo4j 定位为"算法只读 / 异步同步",避免双写一致性。
|
||||
- ⛓️ **依赖 [S3-T1]** 把 `GraphStore` 与 `Retriever` 协议落实,否则会跨层塌方。
|
||||
|
||||
---
|
||||
|
||||
### D4. 对话记忆 ↔ RAG 协同(短期 / 长期 / 检索召回三段桥接)
|
||||
|
||||
> **MemoryBear 的核心特色**。当前最大产品差异化机会就在这里——`core/memory/` 与 `core/rag/` 是 **两条独立链路**,没有联动。
|
||||
|
||||
#### 4.1 触发场景
|
||||
- 用户在第 3 轮说"我对海鲜过敏",第 7 轮问"今晚吃什么?" — 当前 RAG 层无任何记忆能力,每次只看当轮 query。
|
||||
- 多 Agent 协作:售前 Agent 收集到客户预算,售后 Agent 重新询问 — 跨 Agent 记忆需要从 `core/memory` 读出 + 注入 RAG 检索 query 重写。
|
||||
- 长对话上下文压缩:第 50 轮时,前 40 轮对话需要 **被遗忘但保留要点**,要点变成"用户档案 chunk"加入 KB。
|
||||
|
||||
#### 4.2 短期 / 长期 / 检索召回的边界(产品决策)
|
||||
|
||||
| 维度 | 短期记忆(Working Memory) | 长期记忆(Episodic / Semantic) | 检索召回(KB) |
|
||||
|------|---------------------------|--------------------------------|---------------|
|
||||
| 存储位置 | Redis,单 session 8KB cap | Neo4j + ES(`core/memory`) | ES(`core/rag`) |
|
||||
| 生命周期 | session(< 24h) | 永久(按 forgetting curve 衰减) | 永久(人工治理) |
|
||||
| 写入触发 | 每轮 user/assistant message | reflection_engine 周期性提炼 | 文档入库流水线 |
|
||||
| 召回时机 | 始终注入 prompt | LLM 重写 query 时 + 主动检索 | RetrievalNode 命中 |
|
||||
| 数据契约 | `list[Msg]` | `MemoryItem(content, strength, type, ts)` | `DocumentChunk` |
|
||||
| 可信度 | 高(用户原话) | 中(LLM 提炼) | 高(人工审核) |
|
||||
|
||||
> **决策原则**:"用户原话进短期,提炼事实进长期,世界知识进 KB。" 三者不能互相替代。
|
||||
|
||||
#### 4.3 技术方案
|
||||
- **MemoryAugmentedRetriever**:在 `RouterRetriever` 之外再包一层,retrieve 前用 `core/memory.read_services` 拿到当前 user 的 top-K 长期记忆条目,**改写 query**("今晚吃什么?" + 长期记忆"对海鲜过敏" → "今晚吃什么?避免海鲜")。
|
||||
- **Memory Citation**:检索结果与长期记忆条目并入同一 `chunks` 列表,prompt 模板区分两者来源("用户提及" vs "知识库"),避免幻觉混淆。
|
||||
- **反向写入**:每轮对话产出后,让 `core/memory.write_router` 决定 是否需要把"新事实"写入长期记忆;这一步 **复用** `core/memory.agent.langgraph_graph.write_graph`(已存在)。
|
||||
- **遗忘对齐**:把 `core/memory/forgetting_engine` 的 ACT-R 计算复用到 KB chunk 上(D3 已提);让"很少被命中的过期 KB chunk"自动沉睡,反向触发治理团队复审。
|
||||
|
||||
#### 4.4 接口改造点
|
||||
- 在 `workflow/nodes/knowledge/node.py` 的 `KnowledgeRetrievalNode.execute()` 中注入 `MemoryService`:当节点配置里 `enable_memory=true` 时,先调 `memory_service.recall(user_id, query)` 拿记忆,再传给 `Retriever.retrieve(query, ctx={memory: ...})`。
|
||||
- 新增 `MemoryAwareRetriever(Retriever)` 实现,包装任一底层 Retriever。
|
||||
- Workflow Node 配置 `KnowledgeRetrievalNodeConfig` 增加 `memory_strategy: Literal["off", "context_only", "rewrite_query", "merge_chunks"]`。
|
||||
- Prompt 模板新增 `<MEMORY>` 段落。
|
||||
|
||||
#### 4.5 工作量估计
|
||||
- 单向(memory → retrieval):**3 人周**
|
||||
- 双向(retrieval 结果反写 memory):**2 人周**(大部分代码已在 `core/memory` 存在)
|
||||
- 遗忘对齐 + 治理触发:**2 人周**(与 D3 共享)
|
||||
- 合计:**~7 人周**
|
||||
|
||||
#### 4.6 风险与依赖
|
||||
- ⚠️ **隐私边界**:长期记忆是 **per-user**,KB 是 **per-tenant**;混淆会导致跨用户泄露。设计时必须 user_id 级强隔离,code review 重点。
|
||||
- ⚠️ **Prompt 长度膨胀**:记忆 + KB 双源;如果未做摘要,长对话场景 token 成本翻倍;必须配合记忆摘要(已有 `summary4memory.md`)。
|
||||
- ⛓️ **依赖 [S3-T1]** 的 `Retriever / Reranker` 协议;强依赖 [S2-T6] 的 E2E 时序图明确两条链路的衔接点。
|
||||
|
||||
---
|
||||
|
||||
### D5. 评估与反馈闭环(用户反馈 → Reranker 微调)
|
||||
|
||||
#### 5.1 触发场景
|
||||
- 答案错了 / 引用不对,用户点👎 — 当前数据 **进了日志,没人消费**。
|
||||
- 同一 query 在不同时段表现波动 → 需要离线 A/B 评估。
|
||||
- 业务方问"再加一个 KB 之后效果到底变好还是变差?" — 没有可量化的回归指标。
|
||||
- README 给的 F1/BLEU/J 在论文中实现过,**但仓内没有这套代码**,每次评估靠手工。
|
||||
|
||||
#### 5.2 技术方案(双轨:评估在线化 + 反馈学习)
|
||||
|
||||
##### 5.2.1 评估轨:离线 / 在线 / CI 三层
|
||||
|
||||
| 层级 | 内容 | 工具 |
|
||||
|------|------|------|
|
||||
| **离线评估集** | 每 KB 维护一个 `eval_cases.jsonl`:`{query, ideal_chunks, ideal_answer, hard_negatives}`;增量构建(每周从用户问句 + 答疑团队补充) | DSL + Excel 导入工具 |
|
||||
| **在线指标** | `Hit@K / MRR / nDCG / Citation Coverage / Hallucination Rate / Latency P50/P95`;通过 OpenTelemetry 埋点写入 Prometheus | OTel + Prometheus + Grafana |
|
||||
| **CI 评估** | 每个 PR 跑核心 KB 的回归集;指标低于 baseline n% 时阻塞合并 | RAGAS(开源)+ 自研判分 prompt |
|
||||
|
||||
##### 5.2.2 反馈学习轨:从👍/👎到 Reranker 微调
|
||||
|
||||
```
|
||||
用户反馈(👍/👎/edit)
|
||||
↓ event log
|
||||
事件清洗(同一 query 多个 chunk 评分)
|
||||
↓
|
||||
形成 (query, positive_chunk, negative_chunk) 三元组
|
||||
↓
|
||||
├─ 短链:在线 PairWise 调整 BM25/dense 权重(D2 路由器配置)
|
||||
└─ 长链:周/月一次离线训练 Cross-Encoder reranker(基础模型用 bge-reranker-base 蒸馏)
|
||||
↓
|
||||
新 reranker 走 D6 灰度框架上线
|
||||
↓
|
||||
评估轨自动验证收益
|
||||
```
|
||||
|
||||
#### 5.3 接口改造点
|
||||
- 新增 `EvaluationProtocol`:`{evaluate(query, retrieved, generated, ground_truth) -> Metrics}`;在 OpenTelemetry trace 末尾自动落 Prometheus。
|
||||
- `RedBearRerank` 改造:接入 `LocalCrossEncoderRerank(Reranker)` 子类,加载本地 ONNX/TorchScript 模型;可与 Jina/DashScope 并存于工厂。
|
||||
- 反馈采集:复用 `core/memory` 的事件总线(如有)或新建 `feedback_event` 表;前端组件加 thumbs;citation 点击行为也作为隐式反馈。
|
||||
- 训练 pipeline 独立仓 / 独立服务;产物(ONNX)通过模型注册表(用现有 `ModelConfig` 表扩展即可)滚动上线。
|
||||
|
||||
#### 5.4 工作量估计
|
||||
- 评估指标埋点 + Grafana 看板:**1.5 人周**
|
||||
- 离线评估集 + RAGAS CI 集成:**2 人周**
|
||||
- 反馈采集 + 三元组清洗:**1 人周**
|
||||
- Cross-Encoder 蒸馏训练 pipeline:**3 人周**(含数据扩充、训练脚本、产出 ONNX)
|
||||
- 合计:**~7.5 人周**
|
||||
|
||||
#### 5.5 风险与依赖
|
||||
- ⚠️ **冷启动**:刚上线时反馈数据 < 1k 不足以训练;必须先用大模型 LLM-as-Judge 合成训练数据(现成 prompt 在 `prompts/generator.py` 可借鉴)。
|
||||
- ⚠️ **反馈污染**:恶意 / 误点;需要置信度过滤(同一 user 短时多次相反反馈丢弃)。
|
||||
- ⛓️ **依赖 [S3-T1]** 的可观测性方案,否则数据采不到。
|
||||
- ⛓️ **依赖 D2 的语义路由**,否则没有"权重可调"的注入点。
|
||||
|
||||
---
|
||||
|
||||
### D6. 自适应检索路由(Adaptive Retrieval Routing)
|
||||
|
||||
> 这是 D2 中"语义路由"的工程化升级版,独立列出是因为它会**统一**所有检索能力(dense / sparse / graph / memory / web),是 RAG 系统的中央调度器。
|
||||
|
||||
#### 6.1 触发场景
|
||||
- 同一用户在同一 session 内:第 1 个问题需要走 KB,第 2 个问题需要走 Web 搜索("今天的新闻"),第 3 个问题需要 Graph 推理 — 当前必须用户手动切模式。
|
||||
- "你刚才推荐的方案做不了"(指代消解)→ 需要先走对话记忆,再决定是否检索;当前都是无脑全检索。
|
||||
|
||||
#### 6.2 技术方案
|
||||
|
||||
| 决策类型 | 输入 | 输出 |
|
||||
|---------|------|------|
|
||||
| 是否需要检索 | query + 短期记忆 | `bool need_retrieval` |
|
||||
| 检索来源 | query 类型 | `[KB_id, Graph_flag, Web_flag, Memory_flag]` |
|
||||
| 检索策略 | query 类型 + 用户场景 | `(retriever_name, top_k, fusion_weights, rerank_id)` |
|
||||
| 兜底 | 第一次检索结果差 | 触发 query rewriting + 二次检索 |
|
||||
|
||||
实现:
|
||||
- 路由器 = 小型 LLM(如 1.5B–3B)+ rule-based fallback;输出结构化 JSON。
|
||||
- 训练数据来源:D5 的反馈数据 + 标注团队人工标 1k 条。
|
||||
- 推理用 vllm 或 SGLang 自托管,P95 延迟控制在 50ms。
|
||||
|
||||
#### 6.3 接口改造点
|
||||
- 把 `RetrieveType` enum 改造成 strategy(与 D2 共享的 `RouterRetriever`);workflow 层调用方不再选模式,而是传入 query。
|
||||
- 新增 `RoutingPolicy` 配置实体:可被工作空间管理员通过 UI 编辑(默认策略 + 灰度策略)。
|
||||
- 与 D5 形成闭环:评估指标决定路由器升级时机。
|
||||
|
||||
#### 6.4 工作量估计
|
||||
- 规则+LLM 路由器最小可用:**2 人周**
|
||||
- 完整训练 / 灰度 / 配置 UI:**5 人周**
|
||||
- 合计:**~7 人周**
|
||||
|
||||
#### 6.5 风险与依赖
|
||||
- ⚠️ **路由器变成单点**:必须有 fallback 到当前默认策略。
|
||||
- ⛓️ **强依赖 D2 + D5**;不建议独立做。
|
||||
|
||||
---
|
||||
|
||||
## 2. Quick PoC 路径(≤ 1 周可见效果)
|
||||
|
||||
### PoC-A:RRF 多路融合检索(属 D2)
|
||||
|
||||
**目标**:现网 KB 在不重建索引、不改 schema 的前提下,加入 BM25 + dense 各自独立 top-50 → RRF 融合 → 同一接口返回。1 周内拿到 A/B 数据。
|
||||
|
||||
**改动范围**(最小集):
|
||||
- `rag/nlp/search.py:Dealer.search()` 拆为两步:先单独跑 BM25(`emb_mdl=None`),再单独跑 dense(无 BM25),合并时用 RRF。
|
||||
- 增加 feature flag `RETRIEVAL_FUSION_MODE = {"weighted", "rrf"}`,默认 weighted(不影响现网)。
|
||||
|
||||
**预期收益**:在长尾 lookup query 上 Hit@10 +5–10pp(参考社区数据)。无负向风险,因为 weighted 路径保留。
|
||||
|
||||
**PoC 代码草案**(伪代码,约 30 行;正式实现需走完整 PR + 评估):
|
||||
|
||||
```python
|
||||
# rag/retrieval/rrf.py(新增)
|
||||
def rrf_merge(rankings: list[list[ScoredChunk]], k: int = 60, top_k: int = 20) -> list[ScoredChunk]:
|
||||
"""Reciprocal Rank Fusion: score = Σ 1/(k + rank_i)。
|
||||
rankings: 多个独立排序结果,每个内部按相关度降序。
|
||||
"""
|
||||
score_map: dict[str, float] = {}
|
||||
chunk_map: dict[str, ScoredChunk] = {}
|
||||
for ranking in rankings:
|
||||
for rank, chunk in enumerate(ranking, start=1):
|
||||
cid = chunk.metadata["doc_id"]
|
||||
score_map[cid] = score_map.get(cid, 0.0) + 1.0 / (k + rank)
|
||||
chunk_map[cid] = chunk # 保留首次见到的对象
|
||||
merged = sorted(chunk_map.values(),
|
||||
key=lambda c: score_map[c.metadata["doc_id"]],
|
||||
reverse=True)
|
||||
for c in merged:
|
||||
c.metadata["score_rrf"] = score_map[c.metadata["doc_id"]]
|
||||
return merged[:top_k]
|
||||
|
||||
|
||||
# 调用侧(rag/nlp/search.py:Dealer.search 增量改造)
|
||||
if os.getenv("RETRIEVAL_FUSION_MODE", "weighted") == "rrf":
|
||||
bm25_hits = self._search_bm25_only(req, ...)
|
||||
dense_hits = self._search_dense_only(req, ...)
|
||||
return rrf_merge([bm25_hits, dense_hits], k=60, top_k=req.get("topk", 20))
|
||||
# else: 走现有 weighted 路径
|
||||
```
|
||||
|
||||
### PoC-B:Memory-Augmented Query Rewrite(属 D4)
|
||||
|
||||
**目标**:把 `core/memory.read_services` 已有的"长期记忆召回"接到 `KnowledgeRetrievalNode` 之前,做 query 改写。1 周内对 1 个内部 demo 应用上线。
|
||||
|
||||
**改动范围**:
|
||||
- `KnowledgeRetrievalNode.execute()` 第一行加 5 行:拿 user_id(已有 `user_ids`),调 `memory_service.get_user_summary(user_id)`,把 summary 拼到 query 前。
|
||||
- 新增 feature flag `MEMORY_AUGMENT_RETRIEVAL = false`(默认关闭)。
|
||||
- 不改 prompt,不改 schema,不改 ES。
|
||||
|
||||
**预期收益**:在多轮对话场景下,第 N 轮 query 的指代消解正确率提升;无回归风险(flag 默认关)。
|
||||
|
||||
```python
|
||||
# workflow/nodes/knowledge/node.py:KnowledgeRetrievalNode.execute() 头部增量
|
||||
if os.getenv("MEMORY_AUGMENT_RETRIEVAL") == "true" and user_ids:
|
||||
from app.services.user_memory_service import get_user_summary
|
||||
summary = get_user_summary(user_ids[0], ttl_sec=3600) # 已存在 / 类似函数
|
||||
if summary:
|
||||
query = f"[用户背景: {summary}]\n{query}"
|
||||
```
|
||||
|
||||
> **注意**:上述两段代码均为 PoC 草案,真实落地需要:1)完整单测;2)评估对比;3)feature flag 走配置中心;4)权限审查(D4 涉及隐私)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 优先级矩阵(用户价值 × 实现成本 × 风险)
|
||||
|
||||
> 评分 1–5(5 最高 / 5 最低)。建议落地顺序按"用户价值高 + 成本低 + 风险低"加权。
|
||||
|
||||
| 方向 | 用户价值 | 实现成本 (越低越好) | 风险 (越低越好) | 综合分(V × 1/√(C×R)) | 建议落地阶段 |
|
||||
|------|---------|--------------------|----------------|----------------------|------------|
|
||||
| **D2-PoC RRF 融合** | 4 | 5 (0.5 人周) | 5 (无回归) | 8.0 | 立即(Sprint-3 内) |
|
||||
| **D4-PoC Memory Rewrite** | 4 | 5 (0.5 人周) | 4 (隐私) | 7.2 | 立即(Sprint-3 内) |
|
||||
| **D5 评估埋点 + Grafana** | 5 | 4 (1.5 人周) | 5 | 5.6 | 短期(1 月) |
|
||||
| **D5 RAGAS CI** | 4 | 4 | 5 | 4.5 | 短期(1 月) |
|
||||
| **D2 SPLADE 接入** | 4 | 3 (2 人周) | 4 (索引重建) | 3.7 | 短期(1 月) |
|
||||
| **D4 完整双向集成** | 5 | 3 (5 人周) | 3 (隐私 / token) | 3.5 | 中期(2 月) |
|
||||
| **D5 Reranker 微调** | 4 | 3 (3 人周) | 3 (冷启动) | 2.7 | 中期(2 月) |
|
||||
| **D6 自适应路由** | 4 | 2 (5 人周) | 3 | 2.3 | 中期(3 月) |
|
||||
| **D1 多模态 L1(基线)** | 3 | 4 (1.5 人周) | 4 | 3.0 | 短期(1 月) |
|
||||
| **D1 多模态 L2 跨模态** | 5 | 2 (3 人周) | 3 (GPU) | 2.5 | 中期(3 月) |
|
||||
| **D3 增量图演化** | 4 | 2 (3 人周) | 2 (实体消歧) | 2.0 | 中长期(3–4 月) |
|
||||
| **D3 Neo4j 双引擎** | 3 | 2 (3 人周) | 2 (运维) | 1.5 | 长期(4–6 月) |
|
||||
| **D1 多模态 L3 视听统一** | 3 | 1 (4 人周) | 2 (GPU + diarization) | 1.1 | 长期(6 月+) |
|
||||
| **D3 自动本体演化** | 2 | 2 | 2 | 1.0 | 长期 (按需) |
|
||||
|
||||
> **维度说明**
|
||||
> - 用户价值:高优先级业务场景(toB 客户)调研访谈得分。
|
||||
> - 实现成本:人周折算(1 人周=1 分;6 人周=2 分;10 人周=1 分)。
|
||||
> - 风险:含技术风险 + 数据迁移 + 上线回滚 + 安全 / 隐私。
|
||||
> - 综合分用 `V / sqrt(C×R)` 倒数化,**仅作排序参考**,不取代产品/架构会判断。
|
||||
|
||||
---
|
||||
|
||||
## 4. 落地路线图(Roadmap)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title MemoryBear RAG 后续迭代 路线图
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m/%d
|
||||
section Sprint-3 (现 Sprint)
|
||||
PoC-A RRF 融合 (D2) :a1, 2026-06-02, 5d
|
||||
PoC-B Memory Rewrite (D4) :a2, 2026-06-02, 5d
|
||||
section 短期 (1 个月)
|
||||
评估埋点 + Grafana (D5) :s1, 2026-06-09, 7d
|
||||
RAGAS CI (D5) :s2, after s1, 7d
|
||||
SPLADE 接入 (D2) :s3, after s1, 10d
|
||||
多模态 L1 基线 (D1) :s4, 2026-06-09, 7d
|
||||
section 中期 (2-3 个月)
|
||||
Memory ↔ RAG 双向集成 (D4) :m1, after s2, 25d
|
||||
Reranker 微调 pipeline (D5) :m2, after s3, 15d
|
||||
自适应路由 (D6) :m3, after m1, 25d
|
||||
多模态 L2 跨模态 (D1) :m4, after s4, 15d
|
||||
section 长期 (3-6 个月)
|
||||
增量图演化 (D3) :l1, after m1, 20d
|
||||
Neo4j 双引擎 (D3) :l2, after l1, 15d
|
||||
多模态 L3 视听统一 (D1) :l3, after m4, 20d
|
||||
本体演化 (D3) :l4, after l2, 10d
|
||||
```
|
||||
|
||||
> 所有阶段分别绑定一组 OKR + 评估指标(D5 提供数据),未达指标停止下阶段。
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险与依赖总表
|
||||
|
||||
| 类型 | 风险 | 缓解策略 |
|
||||
|------|------|---------|
|
||||
| 架构 | [S3-T1] 接口抽象未落地,本路线图全部方向受阻 | Sprint-3 内先把 `Retriever / Reranker / Embedder / Generator` 4 个 Protocol 落地([S3-T1] 必交付项) |
|
||||
| 数据 | 索引重建(D1/D2/D3)导致服务不可用 | 灰度索引切换工具:双写期 + 流量按租户灰度 + 一键回滚 |
|
||||
| 隐私 | D4 跨用户记忆泄露 | user_id 级强隔离 + 单元测试覆盖 + 上线前安全 review |
|
||||
| 资源 | D1/D6 引入 GPU 依赖 | 优先走托管 API 跑通 PoC;自托管列入 long-term,需要预算评审 |
|
||||
| 治理 | D5 评估集质量低 → CI 阻塞误判 | 评估集双人复核 + 周复盘 + 例外白名单 |
|
||||
| 运维 | D3 Neo4j 双引擎一致性 | 定位 Neo4j 为算法只读,从 ES 异步同步;不双写 |
|
||||
| 业务 | 路线图与产品 PRD 脱节 | 与 [@产品需求分析师] 在 Sprint-3 启动前对齐 1 次 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 与 [S3-T1] / [S3-T3] 的对齐清单
|
||||
|
||||
- ✅ 每个方向都标注了"接口改造点",所有改造均落到 [S3-T1] 提议的 `Retriever / Reranker / Embedder / Generator / GraphStore / Loader` Protocol;不新增其它接口。
|
||||
- ✅ 所有方向有"工作量、风险、依赖"三件套,可被 [S3-T3] 终审按统一模板核对。
|
||||
- ✅ Quick PoC 已覆盖 D2 与 D4 各 1 条(≥ 2 条要求达成)。
|
||||
- ✅ 优先级建议已按"用户价值 × 实现成本 × 风险"三维评分给出,并配有路线图甘特图。
|
||||
- ✅ 多模态、混合搜索、KG 增强、对话记忆、评估闭环均覆盖(5/5);额外补充自适应路由作为联动方向。
|
||||
|
||||
— END —
|
||||
Reference in New Issue
Block a user