知识库改进方案
本文处理知识库从 zhizhan-ai 迁入后遗留的五个问题,按优先级排列:知识应用检索不走索引、入库只有单线程且失败不重试、向量模型没有明确归属、分块策略不能在上传时选择、分类没有在文档页展开。前两个决定知识库能承载多大的数据量,后三个是界面与使用方式。
迁移方案 knowledge-base-convergence.md 已经写过其中三项的要求,交付时没有实现:第 410 行把「KB 默认 embedding 模型 ID/revision;分块/检索策略(保存于 KB 领域配置)」列为业务设置;第 376 行要求「保留线程池、并发上限、失败/重试/取消状态」。cp1–cp3 的验收记录里没有压测或容量测试。本机当前数据是 21 个库、165 个分块向量,本文涉及的容量问题都没有在真实规模下出现过。
| # | 问题 | 现状证据 | 优先级 |
|---|---|---|---|
| 1 | 知识应用检索每次全量加载向量,在 Python 里逐条计算 | kb_app_runtime.py:73-85、:88-89 | P0 |
| 2 | 入库全系统单线程、失败不重试、不能多进程,并依赖一个不调用的 rerank 模型 | kb_pipeline.py:343、389、424、563-583 | P0 |
| 3 | 向量模型由「对接模型」页的排序隐式决定 | aimp_model_client.py:59-66、services/documents.js:44-46 | P1 |
| 4 | 上传时不能选分块策略;分块器支持的参数大部分没有接出来 | kb_pipeline.py:412-421、model/document_management.py:70 | P1 |
| 5 | 分类只是工具栏里的一个下拉框 | DocumentManagement.jsx:198-236 | P2 |
0.1 已确认的决定(2026-10-08,用户)
| # | 决定 | 落在哪一节 |
|---|---|---|
| D1 | 短期不换向量库,继续用 PG + pgvector,把 pgvector 的推荐用法用到位 | 1.3 |
| D2 | 模型调用并发上限缺省 4,可调整,配置放在「平台管理 → 平台设置」 | 2.3 |
| D3 | 更换向量模型期间检索结果不完整,可以接受 | 3.2 |
| D4 | 入库按提交时间先后处理,不按库轮转 | 2.2 |
| D5 | 知识应用跨多个库时,要求这些库使用同一个向量模型 | 3.2 |
| D6 | 不设平台级默认向量模型。向量模型只在知识库页面选择,建库时不预选 | 3.2 |
| D7 | 「对接模型」页不显示模型被几个知识库使用 | — |
| D8 | 不使用半精度向量索引 | 1.3 |
| D9 | 语义分块、情境增强做到与 zhizhan-ai 一致 | 4.4、4.5 |
1. 知识应用检索改走数据库索引
1.1 现状
知识应用(发布后被智能体和工具调用的路径)在 kb_app_runtime.py 的 retrieve() 里按下面的方式检索:
- 查出所选库里全部「可检索」文档的全部分块(第 30-43 行),标签和 metadata 过滤在 Python 里做(第 44-49 行)。
- 查出这些库的全部分块向量(第 73-75 行),逐条算余弦相似度(第 76-85 行)。
- 关键词检索对每个分块做
content.casefold().count(w)(第 88-89 行)。 - 混合模式用 RRF 合并两路排名(常数 61,第 104-111 行)。
数据库里已经有能用的索引,但这条路径一个都没用上:
tb_kb_chunk_embedding.embedding上有 HNSW 索引idx_tb_kb_emb_hnsw(vector_cosine_ops,m=16,ef_construction=256),pgvector 版本 0.8.6。只有「检索和问答测试」走的kb_rag_runtime.py:66-71用了ORDER BY cosine_distance LIMIT。tb_kb_chunk.content_tokens是 tsvector 列,带 GIN 索引ix_tb_kb_chunk_content_tokens,schema 注释写的是「走 PG 全文检索」(schema.py:147)。但没有任何代码写这一列:本机 165 个分块里content_tokens非空的是 0 个。
zhizhan-ai 的检索不是这样做的。它的 kb_retrieve_service.py(945 行)在一条 SQL 里带上库、分类、文档、标签、metadata 过滤,向量召回走 pgvector 余弦 TopN,并按召回条数 SET LOCAL hnsw.ef_search(第 396-412 行);关键词召回用 kb_fts.py(68 行):jieba 搜索引擎模式分词,入库写 to_tsvector('simple', …),查询用 websearch_to_tsquery 的 OR 语义加 ts_rank;两路用 RRF(k=60)融合;另有一路章节向量召回,给命中章节的分块加分(权重 0.15,第 64 行),并为没有分块进入召回的章节补最多 2 条(第 66 行)。迁移到 aimp-kb 时,这套实现被换成了上面的 Python 全量计算,jieba 依赖也去掉了。aimp-kb 入库时仍然计算并保存章节向量(kb_pipeline.py:441,表 tb_kb_section_embedding),但知识应用检索不读它。
推算:一个 1024 维 float4 向量占 4096 字节。库里有 10 万个分块时,每问一个问题要从 PG 传出约 400MB 向量,再加全部分块正文,然后在单个 Python 进程里做 10 万次 1024 维点积。这个数字是按字节数推算的,没有实测。
另一个后果:测试面板与知识应用是两套检索实现。测试面板只有向量检索、rerank 必选;知识应用支持三种模式、rerank 可选、有阈值。在测试面板上调出来的效果,不代表发布后的效果。
1.2 方案
以 zhizhan-ai 的 kb_retrieve_service.py 与 kb_fts.py 为基础移植,按 aimp-kb 的现状做以下适配:
- 向量召回:沿用 zhizhan-ai 的写法,过滤条件全部在 SQL 里,
ORDER BY embedding <=> :q LIMIT :n。增加两项 aimp-kb 特有的条件:向量的embedding_model_id、model_revision等于库的模型(第 3 节),content_version等于文档版本。 - 带过滤的召回:zhizhan-ai 只调了
ef_search。本方案再加SET LOCAL hnsw.iterative_scan = relaxed_order,见 1.3 第 2 项。 - 关键词召回:照搬
kb_fts.py。aimp-kb 加回依赖jieba==0.42.1(zhizhan-ai 用的版本)。入库时由embed_text生成content_tokens,与 schema 注释(schema.py:147、179)一致;查询用websearch_to_tsquery('simple', …)加ts_rank。 - 章节向量召回:照搬,权重 0.15、每章补 2 条两个常量不变。
- 融合:照搬 zhizhan-ai 的 RRF(k=60,单路为空时保留原始分)。aimp-kb 现在用的常数是 61,改为 60。
- 检索模式名称:zhizhan-ai 是 hybrid / vector / fulltext,aimp-kb 知识应用配置里已存的是 hybrid / vector / keyword。保留 aimp-kb 的名称,不改已发布应用的配置。
- 测试面板与知识应用合并为一个检索函数。测试面板传一份临时配置调用同一个函数。
kb_rag_runtime.retrieve删除。 - 存量回填:迁移脚本用 jieba 对现有分块的
embed_text计算content_tokens。本机 165 条,脚本可重复执行。
zhizhan-ai 检索里的「LLM 优化」(用对话模型把命中内容精炼成一段上下文)不在本方案范围内:aimp-kb 的知识应用由回答模型直接引用命中分块,两者作用重叠。
1.3 pgvector 用法规范(D1)
依据是 pgvector 0.8 的官方 README(HNSW、Filtering、Iterative Index Scans、Hybrid Search、Performance 各节)。下表每一项写明本方案怎么做,以及本机现在的值。
| # | 项 | 做法 | 本机现状 |
|---|---|---|---|
| 1 | 查询写法 | ORDER BY embedding <=> :q LIMIT :k。算子必须与索引的 opclass 对应:vector_cosine_ops 对应 <=>。没有 LIMIT、或按距离的表达式排序,都用不上索引 | 列类型 vector(1024)。实测测试面板写的 embedding::vector(1024) 转换会被规划器消掉,EXPLAIN 显示 Index Scan using idx_tb_kb_emb_hnsw |
| 2 | 带过滤的召回 | 事务内 SET LOCAL hnsw.iterative_scan = relaxed_order、SET LOCAL hnsw.ef_search = max(candidate_k, 100);hnsw.max_scan_tuples 保持缺省 20000,超过后迭代扫描停止,返回已找到的结果 | 两个参数都没设;zhizhan-ai 只设了 ef_search |
| 3 | 小库走精确检索 | 保留 kb_id 上的 btree 索引 ix_tb_kb_emb_kb。库很小时,规划器估算按 kb_id 取行再精确排序更便宜,就会走这条路,结果是精确的。不按库建部分索引:库的数量随用户建库增长,部分索引要动态维护 | 索引已存在 |
| 4 | 索引常驻内存 | HNSW 索引要能放进内存,否则每次查询都读盘。按每个向量约 4–5KB 估算(4096 字节向量加图的邻接信息),10 万个分块约 0.5GB,100 万约 5GB。shared_buffers 取机器内存的 25%,effective_cache_size 取 50%–75% | 索引 1184kB / 165 行,样本太小,页开销占比高,不能外推;shared_buffers 128MB,是 PG 缺省值 |
| 5 | 建索引 | maintenance_work_mem 要大于索引大小,否则 pgvector 提示「hnsw graph no longer fits into maintenance_work_mem」,构建明显变慢;max_parallel_maintenance_workers 开并行构建。整库导入时先导数据后建索引 | maintenance_work_mem 64MB,缺省值 |
| 6 | 删除与重建后的整理 | 重新向量化是先删后插,HNSW 里被删的节点要等 VACUUM 清理。对 tb_kb_chunk_embedding 调低 autovacuum 阈值;整库更换向量模型之后执行一次 REINDEX INDEX CONCURRENTLY idx_tb_kb_emb_hnsw | 未设置 |
| 7 | 半精度向量 | 不使用(D8)。内存按全精度索引估算,见第 4 项 | 未使用 |
| 8 | 混合检索 | 向量与全文检索各取 candidate_k,用 RRF 合并。这是 README「Hybrid Search」一节给的做法,也是 zhizhan-ai 的做法,见 1.2 第 3、5 条 | 见 1.1 |
| 9 | 观测 | 启用 pg_stat_statements,按语句看 p95;上线前对检索 SQL 做 EXPLAIN (ANALYZE, BUFFERS),确认走索引、命中缓存 | 未启用 |
第 4、5、6 项是数据库参数,单机安装包的 PG 配置和安装手册要随之更新,按机器内存给出取值。
1.4 被否决的方案
| 做法 | 否决原因 |
|---|---|
| 保留 Python 计算,把向量缓存在进程内存里 | 内存随库大小线性增长;文档更新、删除时要做缓存失效;多进程时每个进程各存一份。 |
| 引入独立向量库(Milvus、Elasticsearch 等) | D1:短期不换。多一个要部署、备份、升级的组件,离线安装包与安装手册都要跟着改。 |
关键词检索沿用 aimp-kb 现在的汉字二元组切词(kb_app_runtime.py:13-21),写进 tsvector | 二元组会产生大量跨词边界的无意义词(「日志不匹配」切出「志不」「不匹」),召回噪声大;zhizhan-ai 原本用的是 jieba,迁移时去掉了,恢复即可。 |
| 关键词检索用 pg_trgm | pg_trgm 对汉字的切分依赖数据库 locale(本机 lc_ctype = en_US.utf8),行为未验证;排序结果与 zhizhan-ai 不一致。 |
1.5 取舍
- 关键词排序从「词频之和」改成 jieba 分词加
ts_rank,单独用关键词模式时排序会变化。混合模式按名次做 RRF,受影响较小。 - 加回章节向量召回后,检索多一条 SQL。
- HNSW 是近似检索,结果与当前的精确计算不完全一致。用评测集对比两种实现的前 10 条重合率,验收阈值为 80%(用户确认)。
- 旧的 Python 实现保留一个版本,用
KB_RETRIEVAL_LEGACY=true切回,用于对比和回退;对比完成后删除。
2. 入库:并发、重试、可多进程
2.1 现状
- 一个后台线程按创建时间逐条处理(
kb_pipeline.py:343、389)。不论哪个库,同一时刻只处理一个文档。 - 向量化按每批 10 条串行调用(
aimp_model_client.py:96,取模型的 maxBatchSize,上限 32)。 - 任何异常都把文档标为失败,
attempts只计数,不重试(kb_pipeline.py:563-583)。 - 取任务不加锁(
run_once第 389 行)。启动时把所有「解析中 / 索引中」的任务重置为排队(第 334-341 行)。这两点都假设只有一个进程,多开进程会重复处理同一个任务。 - uvicorn 是
--workers 1(Dockerfile:14、deploy/kb/compose.yaml:45),后台线程和接口在同一个进程里,切块这类 CPU 计算会占用接口进程的 GIL。 - 入库时取「排第一的已启用 rerank 模型」,只为读它的 maxInputTokens 来限制块长(
kb_pipeline.py:424-432)。没有启用的 rerank 模型时,入库报 422 失败。 - 平台模型接口全局并发上限 4(
AIMP_MODEL_MAX_CONCURRENCY),超出时返回 429CONCURRENCY_LIMIT,带Retry-After: 1(runtime.ts:168-169)。供应商限流返回 429PROVIDER_RATE_LIMIT。两者都标为可重试,但 kb 不重试。
zhizhan-ai 的做法:kb_vectorize_concurrency 缺省 4,线程池并发(vectorization_service.py:265);取任务用 FOR UPDATE SKIP LOCKED(kb_document_service.py:1119)。
2.2 方案
- 取任务:
SELECT … FROM tb_kb_job WHERE status = 'queued' AND (next_run_at IS NULL OR next_run_at <= :now) ORDER BY create_time FOR UPDATE SKIP LOCKED LIMIT 1
取到后在同一事务里写status='parsing'、lease_until = now + 10 分钟、worker_id。tb_kb_job新增next_run_at、lease_until、worker_id三列。 - 租约:每完成一批向量化续租一次。进程启动时不再重置全部任务,只回收
lease_until已过期的任务。进程崩溃后,任务最多 10 分钟后被其他进程重新领取。 - 并发:处理线程数取平台设置「知识库入库并发」,缺省 2,见 2.3。平台模型调用的并发名额由入库和在线问答共用,缺省 4 个时入库占 2 个,在线问答至少剩 2 个。
- 顺序:全局按
create_time先后处理,不按库轮转(D4)。 - 重试,分两层:
- 批次级:单批向量化收到 429 或 503 时,按
Retry-After等待后重发这一批,最多 5 次。不重新解析文档。 - 任务级:批次重试用尽,或调用平台网络失败、docparser 返回 UNAVAILABLE 或 DEADLINE_EXCEEDED 时,任务回到排队,
next_run_at依次为 30 秒、2 分钟、10 分钟后。最多重试 3 次,共执行 4 次;第 4 次失败标为失败(用户确认)。 - 不重试的错误:解析出空文本、文件损坏、422 参数错误、409 模型版本不符。
clean_index()按 document_id 删除旧分块和向量(kb_pipeline.py:153-155),valid()按文档版本校验(第 361-385 行),重复执行不会混写。 - 批次级:单批向量化收到 429 或 503 时,按
- 独立处理进程:配置项
kb_worker_enabled已经存在(core/config.py)。新增入口python -m app.worker,只跑处理线程。接口进程设KB_WORKER_ENABLED=false。compose 增加一个kb-worker服务,用同一个镜像。本机开发保持进程内运行。 - 去掉入库对 rerank 的依赖:块长上限改为
min(向量模型 maxInputTokens, 6000)。检索时已经按 rerank 模型的上限截断送入的文本(kb_rag_runtime.py:93-97、kb_app_runtime.py:125),超长分块只把开头部分送去 rerank。 - 上传改为流式落盘:现在每个文件整体读进内存再写盘(
kb_document_api.py:119,单次最多 20 个、每个 100MB)。改成边读边写、边计数,超过 100MB 时中止并删掉半成品。 - 排队可见:文档列表的「等待处理」状态旁显示「前面还有 N 个」。N 按
create_time早于本任务的排队任务数计算。
2.3 并发配置放进平台设置(D2)
「平台管理 → 平台设置」已有可扩展的设置项机制:键在服务端白名单里(services/platform-setting/index.ts:46 的 TEXT_SETTING_KEYS),读取顺序是数据库 > 环境变量 > 缺省值,保存即生效。现成的同类先例是 RESOURCE_KEY_MAX_RESOURCES(按文本存、按整数 1–200 解释、缺省 50,同文件第 48-57 行)。新增两项,照同样方式实现:
| 键 | 界面名称 | 类型 | 缺省 | 取值 | 越界或为空 | 谁读、何时生效 |
|---|---|---|---|---|---|---|
MODEL_RUNTIME_MAX_CONCURRENCY | 模型调用并发上限 | 整数 | 4(环境变量 AIMP_MODEL_MAX_CONCURRENCY 有值时取它) | 1–64 | 保存时 400;库里的值非法时按缺省值处理 | 平台 model-runtime,每次调用前读,带 30 秒缓存。改小后已在执行的调用不中断,新调用按新上限 |
KB_INGEST_CONCURRENCY | 知识库入库并发 | 整数 | 2 | 1–16,且不大于模型调用并发上限 | 保存时 400,并提示当前的模型调用并发上限 | kb 处理进程每 30 秒拉取一次,增减处理线程;减少时等正在处理的文档完成后再退出线程 |
kb 通过新接口 GET /api/v1/model-runtime/limits 读取这两项,用现有的服务凭据鉴权,返回 {maxConcurrency, kbIngestConcurrency}。平台不可达时 kb 沿用上一次取到的值,启动时取不到就用 1。
平台设置页上「知识库入库并发」旁边写明:入库与在线问答共用模型调用并发名额,入库并发等于上限时,入库高峰期的问答会收到 429 并等待重试。
2.4 被否决的方案
| 做法 | 否决原因 |
|---|---|
| Celery + Redis 任务队列 | kb 多一个依赖和一个部署组件。PG 的 SKIP LOCKED 能满足多进程取任务,任务状态本来就在 PG 里,不用两边对账。 |
| 同一文档内的多个向量化批次并发 | 平台并发名额只有 4。文档之间并发已经能用满名额,文档内再并发只会更快触发 429。 |
| uvicorn 开多个 worker,每个都带处理线程 | 处理线程数随 worker 数翻倍,不可控;接口进程仍然受处理线程的 GIL 影响。 |
| 并发上限按模型分别配置(放在「对接模型」的公共能力字段里) | 平台的并发上限是保护平台自身的总闸,按模型配要把计数器拆成按模型计数,界面多一个字段。供应商各自的限流已经通过 429 和重试处理。出现某个供应商需要比总闸更低的上限时,再按模型加。 |
| 只用环境变量配置 | 改值要重启平台和 kb;D2 要求在平台管理里改。 |
2.5 取舍
- 全局按创建时间排队(D4)。一个库一次上传 200 个文档时,之后提交的其他库的文档排在这 200 个之后。
- 并发计数在平台进程内存里。平台只有一个进程时这就是全局上限;以后平台多实例部署,每个实例各自计数,总并发是上限乘实例数。
- 并发 2 时,入库吞吐的上限由供应商限流和平台并发名额决定,本方案不承诺具体吞吐数字,在第 7 节用实测给出。
3. 向量模型归属到知识库
3.1 现状
- 上传文档时界面只传文件和分类(
services/documents.js:44-46)。kb 用目录里排第一的已启用向量模型(aimp_model_client.py:59-66),顺序就是「对接模型」页的排序。 - 每个分块向量记录了模型 ID 和版本,查询时模型不一致返回 409(
kb_rag_runtime.py:44-47、kb_app_runtime.py:62-70)。 - 测试面板的 Embedding 下拉框能选任意模型,选了入库时没用的模型必然 409。知识应用的向量模型缺省取目录第一个(
appConfig.js:10)。 - 向量列是固定 1024 维的
vector(1024)。 - zhizhan-ai 是一个全局系统设置
ai.default.embedding_model_id,注释为「全平台统一」(zhizhan-aikb_ai_service.py:6),由管理员在系统配置里指定。 - 本机 21 个有向量的库全部是
5b52dde2-8424-4914-816b-4b361a35fe45(KB 百炼向量 text-embedding-v4),版本1-996fc9b78fc8a411。
3.2 方案
tb_kb新增embedding_model_id、embedding_model_revision。- 建库时必选向量模型,不预选(D6)。下拉框列出已启用的 embedding 模型;维度不是 1024 的置灰,并注明原因。没选时「创建」按钮不可点。
- 上传、重新向量化一律使用库的模型。接口参数
embedding_model_id与库的模型不一致时返回 422,不再静默采用。 - 测试面板的 Embedding 改为只读,显示库的模型。
- 知识应用的向量模型改为由所选知识库推出,不再单独选择。所选的多个库模型不一致时,保存失败,并列出模型不同的库名(D5)。
- 更换向量模型:在库的设置页操作。确认框显示要重建的文档数和分块数。确认后写入新模型,全部可检索文档重新排队向量化(只重新切块和向量化,已解析的 Markdown 不重新解析)。重建期间页面顶部显示「重建中 x / y」,还没重建的文档不参与检索,检索结果不完整(D3)。
- 模型版本变化(管理员在「对接模型」页改了模型名、地址、维度等):库的设置页显示「模型配置已变化,需要重建」,提供「按新版本重建」。
3.3 被否决的方案
| 做法 | 否决原因 |
|---|---|
| 沿用 zhizhan-ai 的全局设置,全平台一个向量模型 | 换模型要重建全部库;不同库无法用不同模型。迁移方案第 410 行要求的是按库保存。 |
| 更换模型时新旧两套向量并存,重建完成后切换 | D3 已接受重建期间结果不完整。另外 tb_kb_chunk_embedding 的主键是 chunk_id,一个分块只能存一个向量。并存要改主键并多一倍存储。迁移方案第 421 行已把「无停机索引切换」列为不做。 |
| 知识应用跨模型检索:每种模型各生成一次问题向量,结果合并 | D5 已定不做。不同模型的相似度分数不能直接比较,只能依赖 rerank 合并;每次问答多一次向量调用。 |
3.4 迁移
迁移脚本按库统计可检索文档的 (embedding_model_id, model_revision):
- 只有一组:写入库的模型字段。本机 21 个库都属于这种情况。
- 没有向量:留空。下次上传前,界面要求先在「设置」页签选模型。
- 多于一组:留空,库标记为「模型不一致,需要重建」。
4. 分块策略:库级默认值,上传时可改
4.1 现状
- 上传时不能选。缺省规则:PDF、PPT、PPTX 按页,其他按结构和句子,块长 420、重叠 40(
kb_pipeline.py:412-421)。 - 改分块只能在上传后勾选文档,点「重新向量化」,选项是按结构和句子、按页、整篇不分块,加块长和重叠(
DocumentManagement.jsx:335-341)。 - 分块器
split_by_strategy()(kb_chunking.py:1274)支持 sentence、page、heuristic、semantic、custom、none 六种策略,参数还有标题切分层级、父块上限、自定义分隔符。入库流程调用它时只传了前 4 个参数,接口的chunk_method类型只允许 3 种(model/document_management.py:70)。
| zhizhan-ai | aimp-kb 现在 | 本方案 v1 | |
|---|---|---|---|
| 策略 | 6 种 | 3 种 | 6 种:按结构和句子、按页、启发式、语义、自定义分隔符、整篇不分块 |
| 块长、重叠 | 有 | 有 | 有 |
| 标题切分层级、父块上限 | 有 | 分块器有,没接出来 | 接出来,放在「高级」里 |
| 语义分块 | 有 | 分块器有,没接出来 | 做,与 zhizhan-ai 一致(D9),见 4.4 |
| 情境增强 | 有 | 无(文档表留着 contextual_prefix 列,schema.py:101) | 做,与 zhizhan-ai 一致(D9),见 4.5 |
| 向量化范围(正文 / 标题 / 标题+正文) | 有 | 无 | 不做 |
| 跨库「向量化配置」 | 有 | 无 | 不做,由库级默认值 + 批量重新向量化替代 |
| 在哪里选 | 向量化对话框、向量化配置 | 上传后「重新向量化」 | 库设置(默认值)、上传对话框(本批覆盖)、重新向量化 |
4.2 方案
tb_kb新增chunk_defaults(JSONB),字段见下表。为空时沿用现在的缺省规则。- 库设置页增加「分块默认值」。
- 上传对话框显示「分块:使用知识库默认值(按结构和句子,420 / 40)」,点「本批单独设置」展开表单,只对这一批文件生效,写入每个文档已有的
chunk_method、parser_config、contextual_prefix字段。 - 「重新向量化」使用同一个表单。
- 入库流程把
heading_split_level、parent_max_tokens、custom_delimiter传给split_by_strategy();接口的chunk_method放开到 6 种。
参数约束
| 字段 | 类型 | 必填 | 取值 | 为空或越界 |
|---|---|---|---|---|
| method | string | 是 | sentence / page / heuristic / semantic / custom / none | 422 |
| chunk_size | int | 是 | 32–6000,且不超过向量模型 maxInputTokens | 422 |
| chunk_overlap | int | 是 | 0–1000,小于 chunk_size | 422 |
| heading_split_level | int | 否 | 0–6,0 表示自动识别 | 缺省 0 |
| parent_max_tokens | int | 否 | 0–8000,0 表示整章作为一个父块 | 缺省 0 |
| custom_delimiter | string | method=custom 时必填 | 不超过 200 字符 | method=custom 且为空时 422。分块器本身的行为是回退按标题切分(kb_chunking.py:996),接口层先拦住 |
| contextual_prefix | bool | 否 | true / false | 缺省 false |
| context_model_id、context_model_revision | string | contextual_prefix=true 时必填 | 公共目录里已启用的 chat 模型 | contextual_prefix=true 且为空时 422;模型被停用或版本变化时,入库按「情境增强失败」处理(4.5 失败语义),不阻断向量化 |
4.3 不做的选项
- 向量化范围(仅正文 / 仅标题 / 标题+正文):没有列入本次要求。
- 跨库向量化配置:一份配置作用于多个库和分类,需要配置表、运行状态和进度聚合。库级默认值加批量重新向量化能覆盖「按一套参数处理一批文档」这个用法。
4.4 语义分块(D9)
- 分块函数
split_semantic_in_section在 zhizhan-ai 与 aimp-kb 两边逐字相同(2026-10-08 对比)。算法:相邻句子向量的余弦距离超过分位阈值处断开。 - 缺的只是向量函数:入库时传
deps={"embed": f},f用库的向量模型、经平台模型接口批量生成句向量。 - 句向量生成失败时降级为句子分块,分块器已有这个行为(
kb_chunking.py里的「语义分块 embedding 生成失败,降级为句子分块」警告),降级记入任务记录。 - 代价:每个句子多一次向量计算,按每批 10 条调用。句向量按章节分别生成,一个 500 句的文档至少多 50 次调用,章节越多越多,再加分块本身的向量化。批次重试与并发名额同第 2 节。
4.5 情境增强(D9)
移植 zhizhan-ai 的 contextual_prefix_service.py(519 行)。入库时为每个分块生成一句说明前缀,只拼进 embed_text(因此也进入 content_tokens),不改返回给人和回答模型的 content。常量照搬:
| 常量 | 值 | 含义 |
|---|---|---|
| MAX_CHUNKS_PER_DOC | 300 | 单文档超过这个分块数不生成前缀 |
| MIN_CHUNKS_FOR_PREFIX | 3 | 少于这个分块数整篇跳过 |
| MAX_PREFIX_CHARS | 300 | 前缀长度上限,超出截断 |
| DOC_HEAD_CHARS | 6000 | 文档摘要取正文开头的字符数 |
| MAX_LOCAL_CONTEXT_CHARS | 8000 | 局部窗口(父块或相邻块)字符预算 |
| CONCURRENCY | 4 | 单文档内并发调用的批数 |
| BATCH_MAX_CHUNKS | 6 | 每批最多合并的分块数 |
| BATCH_OUTPUT_TOKENS_PER_CHUNK、BATCH_MIN_OUTPUT_TOKENS | 160、512 | 每批输出预算 |
四级门控照搬,开启情境增强不等于每块调用一次:
- 文档级:分块方式为「按页」「整篇不分块」时整篇跳过。
- 短文档:分块数少于 3 时整篇跳过,文档摘要那次调用也省掉。
- 块级规则:纯标题块、纯图片块、章节整块跳过;表格块不跳过。
- 模型端:提示里给出章节路径,模型判断分块已自足时输出 NONE。
每个文档的调用次数 = 1 次文档摘要 + ⌈通过门控的分块数 ÷ 6⌉ 次批调用。
与 zhizhan-ai 不同的两处:
- 怎么调模型:zhizhan-ai 由系统配置
rag.context_prefix_app_id指定一个「LLM 应用」(应用工厂里 app_type=llm 的应用,自带模型),经_chat()(第 492-519 行)调用。AIMP 没有应用工厂。改为直接调平台公共模型接口的 chat 能力,模型按 D6 在库的「分块默认值」里选。只替换_chat()这一个函数。提示词不用改:文档摘要和批前缀的完整指令(任务说明、NONE 判定规则、禁止事项、输出格式和示例)都写在代码的 user 消息里(第 411 行、第 443 行起),随代码一起移植。 - 并发:单文档内 4 个批并发,加上第 2 节的入库并发,会超过平台模型调用并发上限(缺省 4)。超出的调用收到 429,由 2.2 的批次重试处理。需要更快时在平台设置里调高上限。
失败语义照搬:全程不抛错。文档摘要失败时整篇没有前缀;单批失败时这一批没有前缀;向量化照常完成。每个文档记录跳过、NONE、失败三个计数,文档详情里显示;只有「失败」计数大于 0 才标为警告。
设置表单提供「马上测试」:对选中的一个文档,实时生成前 10 个分块的前缀预览,不写库。zhizhan-ai 的向量化配置表单有同样的功能(KbVectorizeConfigFormView.vue:333)。
5. 分类:文档页左侧分类栏
现状:分类只出现在工具栏的「分类筛选」下拉框、「包含子分类」开关,以及每行文档名下的分类名(KnowledgeBaseDocuments.jsx:280)。zhizhan-ai 是左侧栏:点开知识库后列出分类和各自的文档数(zhizhan-ai KbTab.vue:4-50)。分类树接口已经返回每个分类的文档数(service/document_management.py:81),不用改后端。
- 「文档」页签分左右两栏。左栏宽 236px:「全部文档」「根分类」,下面是分类树,每项带文档数。
- 点父分类时,列表默认包含子分类的文档。「包含子分类」改为列表上方的小开关。
- 库的创建人看到每项的「⋯」菜单(新建子分类、重命名、删除)和底部「新建分类」。其他人只能浏览。
- 上传的目标分类默认取左栏当前选中的分类。
- 工具栏里的「分类筛选」下拉框删除。
- 不做拖拽移动。移动文档沿用现有的「移动」批量操作。
5.1 示意图:文档页
画的是内容区,宽 1220px(实际页面左侧还有约 220px 的导航栏)。库名、文档名、格式、分块数、大小取自本机「CP2 全格式验收-2026-09-13」库。分类名是假设的:本机所有库的文档都在根分类,没有可用的真实分类数据。
| 文档 | 格式 | 状态 | 分块 | 大小 | |
|---|---|---|---|---|---|
| ☐ | cp2-docx.docx Office 文档 | DOCX | 可检索 | 2 | 36.1 KB |
| ☐ | cp2-doc.doc Office 文档 | DOC | 可检索 | 2 | 18.0 KB |
| ☐ | cp2-xls.xls Office 文档 / 表格 | XLS | 可检索 | 1 | 6.0 KB |
| ☐ | cp2-xlsx.xlsx Office 文档 / 表格 | XLSX | 可检索 | 1 | 4.9 KB |
| ☐ | cp2-long-table.xlsx Office 文档 / 表格 | XLSX | 可检索 | 17 | 6.9 KB |
| ☐ | cp2-pptx.pptx Office 文档 | PPTX | 可检索 | 1 | 27.8 KB |
| ☐ | cp2-ppt.ppt Office 文档 | PPT | 等待处理 · 前面还有 3 个 | — | 593.0 KB |
最后一行的「等待处理 · 前面还有 3 个」演示第 2 节的排队提示,这个文档在本机的真实状态是「可检索」。「设置」页签是新增的,承载第 3、4 节的库级设置。
5.2 示意图:上传对话框
左:缺省折叠,只显示库的默认值。右:点「本批单独设置」后展开。文件名取自本机库。
本批单独设置
「输入上限 8192」取自本机 text-embedding-v4 的 maxInputTokens。
5.3 示意图:建库对话框与「设置」页签
左:建库对话框,「标识」「名称」「描述」是现有字段(KnowledgeBaseDialog.jsx:50-69),向量模型和分块默认值是新增的;向量模型未选,所以「创建」不可点。右:设置页签,演示管理员在「对接模型」页改过模型配置之后的状态。模型名与维度取自本机「对接模型」。
右侧的「41 个分块」是本机该库 20 篇文档 chunks_count 之和。横幅只在模型版本变化时出现,本机当前不处于这个状态。
6. 影响面
| 位置 | 改动 |
|---|---|
aimp-kb service/kb_app_runtime.py、kb_rag_runtime.py | 检索合并为一个函数,改为 SQL 召回;kb_rag_runtime.retrieve 删除 |
aimp-kb service/kb_pipeline.py | 取任务、租约、重试、并发线程;用 jieba 写 content_tokens;去掉 rerank 依赖;使用库的模型和分块默认值;语义分块传向量函数 |
| aimp-kb 新增,从 zhizhan-ai 移植 | kb_retrieve_service.py(检索)、kb_fts.py(jieba 全文检索)、contextual_prefix_service.py(情境增强);依赖加 jieba==0.42.1 |
aimp-kb api/kb_document_api.py、kb_management_api.py、model/document_management.py | 上传流式落盘;上传和重新向量化接收分块参数;模型参数不一致返回 422;库的设置接口 |
| aimp-kb alembic | tb_kb 加 3 列,tb_kb_job 加 3 列;回填库的模型与 content_tokens |
aimp-kb 新入口 app/worker.py;meta deploy/kb/compose.yaml、release/compose/docker-compose.yml | 独立处理进程 |
aimp-platform packages/ui/src/views/kb/ | 建库对话框、设置页签、文档页左栏、上传对话框、测试面板的 Embedding 只读、知识应用表单的向量模型改为只读推导 |
aimp-platform server services/platform-setting/、services/model-runtime/、routes/model-runtime/ | 两个平台设置项;并发上限改为读平台设置;新增 GET /model-runtime/limits |
| aimp-platform ui 平台设置页 | 两个设置项的表单与说明 |
meta release/:PG 配置、安装手册 | 按 1.3 第 4、5、6 项给出 PG 参数的取值方法 |
对使用者可见的行为变化:
- 关键词模式的排序变化(1.5)。
- 知识应用不能再单独选向量模型;跨库应用要求所选库的模型一致。
- 通过接口上传时传入与库不一致的
embedding_model_id,从静默采用改为 422。 - 没有启用的 rerank 模型时,文档也能入库。
7. 验证
| 项 | 做法 | 期望 |
|---|---|---|
| 检索延迟 | 生成 1 万和 10 万个分块的合成库,同一组问题分别跑旧实现和新实现,记录 p50、p95,以及 EXPLAIN (ANALYZE, BUFFERS) | 新实现走 idx_tb_kb_emb_hnsw 与 ix_tb_kb_chunk_content_tokens;p95 不随分块数线性增长。没有定容量目标,实测数字写进本文作为基线 |
| 检索一致性 | 评测集上对比新旧实现前 10 条的重合率 | Top10 重合率不低于 80%(用户确认) |
| 带过滤召回 | 在 10 万分块里只选一个 50 分块的库检索,candidate_k=30 | 返回 30 条;关掉 iterative_scan 时少于 30 条(用来证明这项设置生效) |
| 并发入库 | 一次上传 20 个文件 | 同时最多 2 个处于「解析中 / 索引中」,其余显示排队位次,按提交顺序开始;全部完成后 20 个都是「可检索」 |
| 并发热调整 | 入库进行中,在平台设置把「知识库入库并发」从 2 改为 3 | 30 秒内同时处理的文档变为 3 个;不重启平台和 kb |
| 设置校验 | 把「模型调用并发上限」设为 2,再把「知识库入库并发」设为 3 | 保存失败,提示不能大于模型调用并发上限 2 |
| 限流重试 | 在平台设置把「模型调用并发上限」临时设为 1,同时上传 5 个文件,期间在测试面板提问 | 日志里出现 429 后的批次重试,最终全部「可检索」,没有文档被标为失败 |
| 进程崩溃 | 入库中途终止处理进程,再启动 | 租约过期后任务被重新领取,文档最终「可检索」,分块数与单独处理一次时相同 |
| 模型归属 | 建库选 Qwen3-Embedding,上传 1 个文件;之后在「对接模型」页调整排序,再上传 1 个 | 两个文档的向量都是 Qwen3-Embedding;检索不报 409 |
| 语义分块 | 用「语义」方式上传本机 test1 库的「2026美伊战争态势分析报告.md」(按句子分块时 41 块) | 分块边界与按句子分块不同;任务记录里有句向量调用次数,没有降级警告 |
| 情境增强门控 | 开启情境增强,上传「链路详情日志不匹配-现场排查指引.pdf」(按页分块时 4 块),再用「按结构和句子」上传同一文件 | 按页时整篇跳过,没有模型调用;按句子时生成前缀,跳过、NONE、失败三个计数之和等于分块数 |
| 情境增强失败不阻断 | 情境增强模型选一个已停用的 chat 模型后上传 | 文档最终「可检索」,失败计数大于 0,文档详情显示警告 |
| 与 zhizhan-ai 一致 | 同一文档、同一组参数分别在 zhizhan-ai 和 aimp-kb 入库 | 分块数、分块边界一致;情境增强的跳过与生成数一致(前缀文字因模型不同可以不同) |
| 不依赖 rerank | 停用全部 rerank 模型后上传 | 入库成功;检索测试提示需要 rerank 模型 |
8. 迁移与回滚
- 新增列都允许为空,迁移不改已有数据。回滚执行 alembic downgrade 删除新增列,不影响已有向量。
content_tokens回填可重复执行;回滚后这一列重新无人使用,不影响旧实现。- 检索在一个版本内保留
KB_RETRIEVAL_LEGACY=true回退开关。 - 独立处理进程是部署形态的变化。回退方法是删掉
kb-worker服务,把接口进程的KB_WORKER_ENABLED改回true。 - 升级时先停接口进程和处理进程,再执行迁移。排队中的任务保留,启动后继续处理。
9. 实施顺序
- 第 1 节(检索)。独立,可以先做;先做合成数据压测,拿到旧实现的基线。
- 第 2 节(入库),含 2.3 的两个平台设置项。与第 1 节没有依赖,可以并行。
- 第 3、4 节一起做:两者都改
tb_kb、库的设置接口和「设置」页签,合并成一次迁移。4.5 情境增强最后做。 - 第 5 节(分类栏)。纯前端,随时可做。
10. 未决问题
前两轮的 12 项已由 0.1 节的 9 项决定关闭。容量没有给出具体数字,以第 7 节的实测结果作为基线。上一版列的「情境增强系统提示词的来源」不成立:完整指令在代码的 user 消息里(4.5 节)。剩一项不阻塞开工的核对:
| # | 问题 | 影响 | 谁定,何时必须定 |
|---|---|---|---|
| 1 | zhizhan-ai 部署环境里那个「LLM 应用」是否另外配了系统提示词。仓库里看不到应用配置;移植后不设系统提示词 | 如果那边配了额外要求,两边生成的前缀风格会有差别;不影响门控、合批和失败处理 | 【待确认】能访问 zhizhan-ai 部署库的人查看该应用配置;在第 7 节「与 zhizhan-ai 一致」那项验证时对比即可 |