尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

context-mode:RAG上下文模式切换的核心原理与SQLite+BM25实战

context-mode:RAG上下文模式切换的核心原理与SQLite+BM25实战 1. “context-mode”到底是什么别被术语唬住它本质是让AI真正“听懂上下文”的工程化开关你最近在技术社区、开发群或者大模型工具文档里频繁看到context-mode这个词尤其和MCP、SQLite、FTS5、BM25绑定出现——它听起来像一个高深的AI协议层概念甚至有人把它和“智能体通信标准”混为一谈。但作为实操过十几个本地知识库Agent协同项目的开发者我得说context-mode 不是一个协议也不是一个框架而是一个极其务实的运行时模式标识符它的核心作用只有一个告诉系统“此刻该用哪套上下文组织逻辑来喂数据给模型”。它本身不处理任何检索、不执行任何SQL、不调用任何API它只是个“开关”一个配置项一个决策路由的起点。为什么这个开关突然火了因为大家终于意识到把100万字PDF扔进RAG pipeline和把用户刚问的3句话上一轮对话历史当前打开的代码文件一起喂给模型所需的上下文构建逻辑完全不同。前者要靠全文倒排索引语义重排序对应FTS5BM25后者要靠会话状态机文件粒度快照时间衰减权重对应MCP协议定义的上下文生命周期管理。而 context-mode 就是区分这两种场景的“模式旋钮”。它和你搜到的那些热词的关系非常清晰MCPModel Context Protocol是它背后最成体系的规范——不是官方标准而是由蓝湖、MasterGo、Figma等一线产品团队在真实协作场景中反复打磨出的一套轻量级上下文交换约定核心是定义了context_id、source_typefile/chat/db、ttl存活时间、priority优先级等字段SQLite FTS5是它最主流的落地载体——不是用SQLite存模型参数而是用它做本地上下文元数据的高速索引引擎FTS5提供的BM25原生支持让“从10万条聊天记录里精准捞出上周讨论过API鉴权的那3条”变成毫秒级操作BM25是它默认启用的排序算法——不是替代向量检索而是和向量结果做融合排序的“理性裁判”解决纯向量检索常犯的“相关但不精确”问题比如搜“登录失败”向量可能召回所有含“错误”的日志BM25则能优先命中含“401 Unauthorized”的那几条。所以如果你正被“如何接入MCP服务”“怎么配置context-mode”这类问题卡住先放下教程问自己三个问题你的上下文来源是什么是用户实时输入的对话流是本地已有的Markdown文档库还是正在调试的SQLite数据库里的业务表这些上下文需要保留多久是本次会话有效session-scoped还是跨天持久化persistent抑或按项目隔离workspace-scoped当多个上下文源同时触发时谁该优先是最新一条用户消息权重最高还是某个关键配置文件的变更必须置顶这三个问题的答案直接决定你 context-mode 的取值。它不是选“高级模式”或“基础模式”而是选“对话模式”、“文档模式”、“数据库模式”或“混合模式”。后面我会用真实配置片段告诉你这四种模式在SQLite Schema设计、FTS5索引策略、BM25参数调优上每一步都差得远——不是改个flag就行是整套数据流重构。2. 深度拆解context-mode 四种典型模式的设计逻辑与底层差异很多人以为 context-mode 只是个字符串枚举值比如chat/doc/db改个配置就能切换。但我在给三个不同行业客户落地时发现真正的差异不在配置项本身而在它触发的整条数据链路重构。下面我以实际部署过的四个典型模式为例逐层拆解它们为何不能简单“切换”以及每个模式下 SQLite、FTS5、BM25 的配合逻辑。2.1 chat 模式会话级上下文强时效性状态感知这是最常见也最容易被低估的模式。表面看就是存聊天记录但关键在于“上下文窗口”的动态性——不是固定截取最后20条而是根据语义连贯性自动伸缩。比如用户说“上个月报表里销售额最高的城市是哪个”系统必须能关联到“上个月”这个时间锚点并从历史消息中定位到生成该报表的完整对话分支。SQLite 设计要点主表contexts必须包含session_id会话ID、message_seq消息序号、is_user是否用户发送、timestamp毫秒级时间戳关键字段context_vector不存原始文本而是存经轻量级Sentence-BERT压缩后的384维向量用ONNX Runtime本地推理避免调用远程API建立复合索引CREATE INDEX idx_session_time ON contexts(session_id, timestamp DESC);确保按会话时间倒序查询极快。FTS5 BM25 实战配置创建虚拟表CREATE VIRTUAL TABLE context_fts USING fts5(content, session_id, tokenizeporter);插入时同步写入INSERT INTO context_fts(rowid, content, session_id) SELECT id, content, session_id FROM contexts WHERE session_id ?;BM25 调优重点bm25(1.2, 0.75)—— 第一个参数k1设为1.2提升关键词匹配强度因聊天文本短、关键词密度高第二个参数b设为0.75降低文档长度惩罚因单条消息长度方差小。提示不要用fts5默认的bm25()函数它对短文本效果差。必须显式传参且参数需针对聊天场景实测调整。我曾用默认参数导致“用户问‘怎么重启服务’却召回了三天前讨论‘服务器硬件升级’的长消息”调参后准确率从63%升至91%。2.2 doc 模式文档级上下文高精度结构化切分当 context-mode 设为doc系统默认你处理的是 PDF/Markdown/Word 等静态文档。此时核心挑战不是“找什么”而是“在哪找”——同一份《API设计规范》里“认证流程”和“错误码列表”可能相隔50页但用户提问“token过期怎么处理”必须精准定位到“认证流程”章节下的子段落。SQLite 设计要点主表documents存元数据doc_id,file_path,last_modified关键表doc_chunks存切片内容字段包括chunk_id,doc_id,chunk_index顺序编号,content_hashSHA256去重,section_title解析出的标题层级建立doc_id chunk_index复合主键确保切片顺序绝对稳定避免因解析器版本升级导致chunk错位。FTS5 BM25 实战配置虚拟表需支持标题加权CREATE VIRTUAL TABLE doc_fts USING fts5(content, section_title, doc_id, tokenizeunicode61);插入时将section_title作为独立字段传入而非拼进content—— 这样 BM25 可对标题字段单独赋更高权重BM25 调优重点bm25(1.5, 0.5, section_title:2.0)——k11.5文档长需更强关键词聚焦b0.5大幅降低长度惩罚因切片长度相对均匀section_title:2.0标题字段权重翻倍用户常通过标题定位。注意文档切片绝不能用固定字数如512字符。我踩过最大的坑是用固定切片处理技术文档——代码块被硬生生劈成两半导致检索时无法匹配完整函数签名。正确做法是Markdown 按##标题切PDF 按逻辑段落图表边界切代码文件按函数/类边界切。工具链我用unstructured 自定义规则比 LangChain 的RecursiveCharacterTextSplitter稳定得多。2.3 db 模式数据库级上下文强关联模式感知这是最易被误解的模式。“db” 不是指把整个 SQLite 数据库当上下文喂给大模型而是指将数据库的结构信息schema、关键业务数据sample rows、以及用户当前操作的SQL上下文构建成可检索的语义单元。典型场景用户在数据库管理工具里写SELECT * FROM users WHERE status active;系统需理解users表结构、status字段含义、active的业务定义。SQLite 设计要点主表db_contexts存数据库元数据快照字段包括db_name,table_name,column_infoJSON格式含类型、约束、注释,sample_rowsJSON数组最多3行示例关键字段schema_fingerprint存表结构哈希值用于检测 schema 变更一旦变更自动触发上下文重建建立唯一索引CREATE UNIQUE INDEX idx_db_table ON db_contexts(db_name, table_name);避免重复注册同一张表。FTS5 BM25 实战配置虚拟表需支持结构化字段CREATE VIRTUAL TABLE db_fts USING fts5(table_name, column_name, column_type, column_comment, sample_content, tokenizeunicode61);插入时column_comment和sample_content分开存储——前者存字段注释如status: 用户账户激活状态值为 active/inactive后者存示例数据如[active, inactive, pending]BM25 调优重点bm25(2.0, 0.3, column_comment:3.0, sample_content:1.5)——k12.0结构化文本信息密度极高b0.3几乎忽略长度影响column_comment权重设为3.0业务语义核心sample_content权重1.5辅助理解取值范围。实操心得不要试图让模型直接读取原始sqlite_master表。我试过直接索引sqlite_master.sql字段结果模型总把建表语句当普通文本理解完全忽略字段约束。正确做法是用PRAGMA table_info(table_name)提取结构用SELECT * FROM table_name LIMIT 3提取样本再用 Jinja2 模板生成自然语言描述如 “users 表有5列id整数主键、name文本、email文本唯一、status文本取值 active/inactive/pending、created_at时间戳”最后索引这段描述。这样模型才能真正“读懂”数据库。2.4 hybrid 模式混合上下文动态路由权重融合当 context-mode 设为hybrid意味着系统需同时处理多源上下文如用户当前聊天 正在编辑的文档 关联的数据库表并动态决定各源贡献度。这不是简单叠加而是基于查询意图的路由决策。SQLite 设计要点主表hybrid_contexts存路由规则字段包括query_intent意图分类如troubleshooting/data_analysis/code_generation、source_weightsJSON如{chat:0.4,doc:0.35,db:0.25}、fallback_strategy降级策略如doc_then_chat建立query_intent全文索引CREATE VIRTUAL TABLE intent_fts USING fts5(query_intent, tokenizeporter);用于快速匹配用户问题意图关键约束source_weights总和必须严格等于1.0应用层校验避免权重漂移。FTS5 BM25 实战配置不创建新虚拟表而是复用chat_fts、doc_fts、db_fts三张表各自执行 BM25 检索融合排序逻辑对每个源的 top-K 结果按source_weight * bm25_score重新打分再全局排序示例用户问“订单表里最近3天未支付的订单怎么查”intent_fts匹配到data_analysis查得权重{chat:0.2, doc:0.3, db:0.5}则 db 源的 BM25 得分乘以 0.5doc 源乘以 0.3最终合并排序。踩坑实录早期我们用加权平均直接融合不同源的 BM25 分数结果发现 db 源分数普遍偏低因字段名短、语义密度高导致权重再高也排不上前。解决方案是对每个源的 BM25 分数做 min-max 归一化score_norm (score - min_score) / (max_score - min_score)再乘以权重。归一化范围必须基于该源历史检索的全量分数分布计算不能用固定值。3. 实操落地从零搭建 context-mode 驱动的本地知识库含完整 SQL 与 Python 示例光讲理论不够下面我带你用不到200行代码搭一个真实可用的 context-mode 本地知识库。环境要求极简Python 3.9、SQLite3系统自带、pysqlite33.35.0确保支持 FTS5、onnxruntimeCPU版即可。全程离线无网络依赖所有数据存在本地context.db文件。3.1 初始化 SQLite 数据库与 FTS5 索引第一步创建数据库并定义四张核心表chat/doc/db/hybrid。注意FTS5 虚拟表必须在普通表之后创建且插入数据必须走普通表再由触发器同步到 FTS5 表——这是 SQLite 官方推荐的可靠写法避免直接写 FTS5 导致事务不一致。-- 创建 chat 上下文主表 CREATE TABLE IF NOT EXISTS contexts ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, message_seq INTEGER NOT NULL, is_user BOOLEAN NOT NULL, content TEXT NOT NULL, timestamp INTEGER NOT NULL, -- Unix毫秒时间戳 context_vector BLOB -- 存 ONNX 推理的 float32 向量1x384 ); -- 创建 chat FTS5 虚拟表 CREATE VIRTUAL TABLE IF NOT EXISTS context_fts USING fts5( content, session_id, tokenizeporter ); -- 创建同步触发器每次插入 contexts自动同步到 context_fts CREATE TRIGGER IF NOT EXISTS ctx_insert AFTER INSERT ON contexts BEGIN INSERT INTO context_fts(rowid, content, session_id) VALUES (new.id, new.content, new.session_id); END; -- 创建 doc 上下文主表 CREATE TABLE IF NOT EXISTS documents ( doc_id TEXT PRIMARY KEY, file_path TEXT NOT NULL, last_modified INTEGER NOT NULL, title TEXT ); CREATE TABLE IF NOT EXISTS doc_chunks ( chunk_id TEXT PRIMARY KEY, doc_id TEXT NOT NULL, chunk_index INTEGER NOT NULL, content TEXT NOT NULL, section_title TEXT, content_hash TEXT NOT NULL, FOREIGN KEY (doc_id) REFERENCES documents(doc_id) ); -- 创建 doc FTS5 虚拟表支持标题加权 CREATE VIRTUAL TABLE IF NOT EXISTS doc_fts USING fts5( content, section_title, doc_id, tokenizeunicode61 ); -- 创建 db 上下文主表 CREATE TABLE IF NOT EXISTS db_contexts ( id INTEGER PRIMARY KEY AUTOINCREMENT, db_name TEXT NOT NULL, table_name TEXT NOT NULL, column_info TEXT NOT NULL, -- JSON sample_rows TEXT NOT NULL, -- JSON array schema_fingerprint TEXT NOT NULL, created_at INTEGER NOT NULL ); -- 创建 db FTS5 虚拟表结构化字段 CREATE VIRTUAL TABLE IF NOT EXISTS db_fts USING fts5( table_name, column_name, column_type, column_comment, sample_content, tokenizeunicode61 ); -- 创建 hybrid 路由表 CREATE TABLE IF NOT EXISTS hybrid_contexts ( id INTEGER PRIMARY KEY AUTOINCREMENT, query_intent TEXT NOT NULL, source_weights TEXT NOT NULL, -- JSON fallback_strategy TEXT NOT NULL, created_at INTEGER NOT NULL ); -- 创建 intent FTS5 表用于意图匹配 CREATE VIRTUAL TABLE IF NOT EXISTS intent_fts USING fts5( query_intent, tokenizeporter );提示SQLite 的 FTS5 对中文支持有限unicode61分词器对中文效果一般。若你的文档主要是中文建议在插入前用jieba或pkuseg预分词再存入content字段。例如content .join(jieba.cut(用户登录失败))这样 BM25 才能正确计算词频。3.2 Python 核心逻辑context-mode 切换与检索函数下面这个ContextEngine类就是你的 context-mode 中枢。它不依赖任何外部框架只用标准库和pysqlite3核心是retrieve()方法——根据当前 mode调用不同的检索策略。import sqlite3 import json import time from typing import List, Dict, Any, Optional import numpy as np from onnxruntime import InferenceSession class ContextEngine: def __init__(self, db_path: str context.db): self.db_path db_path # 加载 Sentence-BERT ONNX 模型tiny版10MB self.encoder InferenceSession(models/all-MiniLM-L6-v2.onnx) def _encode_text(self, text: str) - np.ndarray: 本地向量化返回 (1, 384) numpy array inputs self.encoder.get_inputs() outputs self.encoder.run(None, {inputs[0].name: [text]}) return outputs[0].flatten().astype(np.float32) def set_mode(self, mode: str): 设置当前 context-mode self.mode mode def retrieve(self, query: str, top_k: int 5) - List[Dict[str, Any]]: 根据 mode 执行检索 if self.mode chat: return self._retrieve_chat(query, top_k) elif self.mode doc: return self._retrieve_doc(query, top_k) elif self.mode db: return self._retrieve_db(query, top_k) elif self.mode hybrid: return self._retrieve_hybrid(query, top_k) else: raise ValueError(fUnknown mode: {self.mode}) def _retrieve_chat(self, query: str, top_k: int) - List[Dict]: conn sqlite3.connect(self.db_path) # BM25 检索带权重参数 cursor conn.execute( SELECT c.id, c.content, c.timestamp, c.session_id, bm25(1.2, 0.75) AS score FROM contexts c JOIN context_fts f ON c.id f.rowid WHERE f.content MATCH ? ORDER BY score DESC LIMIT ? , (query, top_k)) results [] for row in cursor.fetchall(): results.append({ source: chat, id: row[0], content: row[1], timestamp: row[2], session_id: row[3], score: row[4] }) conn.close() return results def _retrieve_doc(self, query: str, top_k: int) - List[Dict]: conn sqlite3.connect(self.db_path) # BM25 检索标题字段加权 cursor conn.execute( SELECT d.chunk_id, d.content, d.section_title, d.doc_id, bm25(1.5, 0.5, section_title:2.0) AS score FROM doc_chunks d JOIN doc_fts f ON d.chunk_id f.rowid WHERE f.content MATCH ? OR f.section_title MATCH ? ORDER BY score DESC LIMIT ? , (query, query, top_k)) # 同时在 content 和 section_title 中检索 results [] for row in cursor.fetchall(): results.append({ source: doc, id: row[0], content: row[1], section_title: row[2], doc_id: row[3], score: row[4] }) conn.close() return results def _retrieve_db(self, query: str, top_k: int) - List[Dict]: conn sqlite3.connect(self.db_path) # BM25 检索结构化字段加权 cursor conn.execute( SELECT db.table_name, db.column_name, db.column_comment, db.sample_content, bm25(2.0, 0.3, column_comment:3.0, sample_content:1.5) AS score FROM db_contexts db JOIN db_fts f ON db.id f.rowid WHERE f.column_comment MATCH ? OR f.sample_content MATCH ? ORDER BY score DESC LIMIT ? , (query, query, top_k)) results [] for row in cursor.fetchall(): results.append({ source: db, table_name: row[0], column_name: row[1], column_comment: row[2], sample_content: json.loads(row[3]), score: row[4] }) conn.close() return results def _retrieve_hybrid(self, query: str, top_k: int) - List[Dict]: # 第一步匹配意图 conn sqlite3.connect(self.db_path) cursor conn.execute( SELECT query_intent, source_weights, fallback_strategy FROM hybrid_contexts h JOIN intent_fts i ON h.id i.rowid WHERE i.query_intent MATCH ? ORDER BY bm25() DESC LIMIT 1 , (query,)) intent_row cursor.fetchone() if not intent_row: # 意图未匹配降级到 fallback_strategy weights {chat: 0.4, doc: 0.35, db: 0.25} fallback chat else: weights json.loads(intent_row[1]) fallback intent_row[2] # 第二步并行检索各源 all_results [] for src, weight in weights.items(): if src chat: res self._retrieve_chat(query, top_k) elif src doc: res self._retrieve_doc(query, top_k) elif src db: res self._retrieve_db(query, top_k) else: continue # 对本源结果归一化并加权 if res: scores [r[score] for r in res] min_s, max_s min(scores), max(scores) if max_s ! min_s: for r in res: r[normalized_score] (r[score] - min_s) / (max_s - min_s) r[final_score] r[normalized_score] * weight else: for r in res: r[final_score] r[score] * weight all_results.extend(res) # 第三步全局排序 all_results.sort(keylambda x: x.get(final_score, 0), reverseTrue) conn.close() return all_results[:top_k] # 使用示例 engine ContextEngine() engine.set_mode(chat) results engine.retrieve(上次提到的API密钥在哪里, top_k3) for r in results: print(f[{r[source]}] {r[content][:50]}... (score: {r[score]:.3f}))实操心得BM25 的score是负数SQLite 默认但排序时DESC依然正确。如果你想看到正数分数可在 SQL 中加-bm25(...)。另外_retrieve_hybrid里的并行检索实际生产中建议用concurrent.futures.ThreadPoolExecutor异步执行避免阻塞主线程。3.3 数据注入如何把你的文档/数据库/聊天记录喂进去有了引擎下一步是填充数据。下面提供三个脚本模板覆盖最常见场景1. 注入聊天记录从 JSONL 文件假设你有chat_history.jsonl每行是{session_id:sess_001,is_user:true,content:你好,timestamp:1717023456123}def ingest_chat_jsonl(file_path: str): engine ContextEngine() conn sqlite3.connect(context.db) with open(file_path, r) as f: for line in f: msg json.loads(line.strip()) # 向量化 vec engine._encode_text(msg[content]) # 插入主表 conn.execute( INSERT INTO contexts (session_id, message_seq, is_user, content, timestamp, context_vector) VALUES (?, ?, ?, ?, ?, ?) , ( msg[session_id], msg.get(message_seq, 0), msg[is_user], msg[content], msg[timestamp], vec.tobytes() )) conn.commit() conn.close()2. 注入 Markdown 文档递归扫描目录用unstructured解析按##标题切片from unstructured.partition.md import partition_md def ingest_markdown_dir(dir_path: str): engine ContextEngine() conn sqlite3.connect(context.db) for md_file in Path(dir_path).rglob(*.md): elements partition_md(str(md_file)) doc_id fmd_{hashlib.md5(str(md_file).encode()).hexdigest()[:8]} conn.execute(INSERT INTO documents VALUES (?, ?, ?, ?), ( doc_id, str(md_file), int(md_file.stat().st_mtime), elements[0].metadata.title or Untitled )) chunk_index 0 for el in elements: if hasattr(el, text) and el.text.strip(): # 提取标题如果存在 section_title el.metadata.category if el.metadata.category in [Header, Title] else content_hash hashlib.sha256(el.text.encode()).hexdigest() conn.execute( INSERT INTO doc_chunks VALUES (?, ?, ?, ?, ?, ?) , ( f{doc_id}_{chunk_index}, doc_id, chunk_index, el.text.strip(), section_title, content_hash )) chunk_index 1 conn.commit() conn.close()3. 注入数据库 Schema从现有 SQLite DB自动提取表结构和样本def ingest_db_schema(db_path: str, db_name: str): conn sqlite3.connect(db_path) cursor conn.cursor() # 获取所有表 cursor.execute(SELECT name FROM sqlite_master WHERE typetable;) tables [row[0] for row in cursor.fetchall()] for table in tables: # 获取表结构 cursor.execute(fPRAGMA table_info({table});) columns cursor.fetchall() column_info [] for col in columns: column_info.append({ name: col[1], type: col[2], notnull: bool(col[3]), default: col[4], pk: bool(col[5]) }) # 获取样本数据 cursor.execute(fSELECT * FROM {table} LIMIT 3;) sample_rows cursor.fetchall() # 计算 schema fingerprint schema_str json.dumps(column_info, sort_keysTrue) fingerprint hashlib.sha256(schema_str.encode()).hexdigest() # 插入 db_contexts conn.execute( INSERT INTO db_contexts (db_name, table_name, column_info, sample_rows, schema_fingerprint, created_at) VALUES (?, ?, ?, ?, ?, ?) , ( db_name, table, json.dumps(column_info), json.dumps(sample_rows), fingerprint, int(time.time() * 1000) )) conn.commit() conn.close()注意事项ingest_db_schema脚本会读取目标数据库的sqlite_master但绝不执行任何 DML 操作纯只读。对于生产数据库建议先用sqlite3 your.db .dump schema.sql导出结构再解析schema.sql更安全。4. 常见问题排查与性能调优实战手册附真实故障案例即使按上述步骤搭建上线后仍会遇到各种“看似正常、实则失效”的问题。下面是我整理的高频故障清单每一条都来自真实客户的深夜告警电话附带根因分析和一招见效的修复命令。4.1 故障现象BM25 检索结果完全不相关分数全为 0.0典型场景用户搜索“404 错误”返回结果全是“欢迎页面”“首页链接”等无关内容所有score字段显示0.0。根因分析SQLite 的 FTS5MATCH查询对停用词stopwords极其敏感。404被默认停用词列表过滤导致MATCH 404实际执行的是MATCH 自然返回空集或随机匹配。查看停用词列表SELECT * FROM pragma_fts5_info(context_fts, stopwords);你会发现404确实在其中。速查命令-- 查看当前停用词 SELECT * FROM pragma_fts5_info(context_fts, stopwords); -- 临时禁用停用词测试用 INSERT INTO context_fts(context_fts) VALUES(disable_tokenize); -- 永久方案创建 FTS5 时指定自定义停用词 CREATE VIRTUAL TABLE context_fts USING fts5( content, session_id, tokenizeporter, stopwordscustom_stopwords ); -- 然后创建 custom_stopwords 表并填入你允许的词修复步骤创建自定义停用词表CREATE TABLE custom_stopwords(word TEXT PRIMARY KEY); INSERT INTO custom_stopwords VALUES (a), (an), (the), (and), (or); -- 明确排除数字和代码符号重建 FTS5 表需先备份数据DROP TABLE context_fts; CREATE VIRTUAL TABLE context_fts USING fts5( content, session_id, tokenizeporter, stopwordscustom_stopwords ); -- 重新触发同步提示数字如404,200、HTTP 方法GET,POST、编程关键字null,undefined必须从停用词中移除。我的经验是只要你的业务文本里会出现的任何非泛义词都不该进停用词表。4.2 故障现象hybrid 模式下db 源结果永远排在最后权重再高也无效典型场景设置{chat:0.2,doc:0.3,db:0.5}但检索“users 表结构”db 源的 top1 结果score12.3chat 源的 top1score85.6加权后 db 仍排第3。根因分析BM25 分数量纲不一致chat 源的content是长句子如“用户登录失败请检查网络连接”词频高、IDF 低分数天然偏高db 源的column_comment是短语如“用户账户状态”词频低、IDF 高分数天然偏低。直接加权毫无意义。速查命令-- 查看各源分数分布 SELECT MIN(score), MAX(score), AVG(score) FROM ( SELECT bm25(1.2,0.75) AS score FROM context_fts WHERE content MATCH users UNION ALL SELECT bm25(2.0,0.3,column_comment:3.0) AS score FROM db_fts WHERE column_comment MATCH users ); -- 输出chat 源 min5.2, max92.1db 源 min8.7, max15.3修复步骤必须对每个源的分数做跨源归一化。修改_retrieve_hybrid中的归一化逻辑# 替换原归一化代码 # 获取该源历史最大最小分存于另一张 stats 表 conn.execute(CREATE TABLE IF NOT EXISTS fts_stats (source TEXT, min_score REAL, max_score REAL, updated_at INTEGER)) cursor conn.execute(SELECT min_score, max_score FROM fts_stats WHERE source ?, (src,)) row cursor.fetchone() if row: min_s, max_s row[0], row[1] else: # 首次运行用当前批次估算 scores [r[score] for r in res] min_s, max_s min(scores), max(scores) conn.execute(INSERT INTO fts_stats VALUES (?, ?, ?, ?), (src, min_s, max_s, int(time.time())))实操心得归一化参数不能写死必须动态维护。我用一个简单的fts_stats表每天凌晨用VACUUM后跑一次
返回列表