用户需求:客户上传 PDF 等资料进行向量化并追加进向量库,且问答可检索。 决策:全局检索融合 / BFF 直连 DeepSeek 生成私有回答 / 两路并行展示(命中才显示) / 「我的资料」管理界面挂文档 TAB 空态。改动仅限本分支,QA 金标树与红线零改动。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.7 KiB
7.7 KiB
任务书 · A1(编码 agent)—— 私有资料问答 P1-P3 实现
docs/team/brief_priv_a1.md | 开工前必读 docs/private_docs_team_context.md(改动清单/红线/纪律是硬约束)。 权威规格:docs/PRD.md §17(FR-PRIV-001~008)、docs/UI_SPEC.md §9、实施方案(TL 已批准)。
任务
按三块实现「客户上传资料 → 问答可检索」闭环。全部代码写在服务器仓库 /home/xzq/5090/teamWorkspace/zh/PlantAI-rag-new(feature/bioflow-conversation-orchestration 分支)。不 commit、不部署、不碰清单外文件。
P1 vstore chunk 级索引(最大块)
- 新
vstore/chunks.py:- 切块参数常量:
CHUNK_TARGET_CHARS=600、CHUNK_MIN_CHARS=120、CHUNK_MAX_PARAS=8、CHUNK_OVERLAP_PARAS=1、CHUNK_MAX_PER_DOC=200、CHUNK_MIN_SCORE=0.30、CHUNK_TOP_K_MAX=20。 build_chunks(title, outline, paragraphs, tables) -> list[Chunk]:段落顺序聚合(≥600 字或满 8 段封块);相邻块重叠 = 前块最后 1 段作下块前缀;表格矩阵化为|a|b|行式文本段参与;首块前缀# {title};seq 从 0 起;截断 200 块。Chunk = dataclass(seq, text),chunk_id ={doc_id}:{seq}。class ChunkIndex:三文件(index.jsonmeta 行 /index.faissfaiss 二进制 /texts.jsonl文本);load();upsert_document()(替换语义:先移旧行过滤重建 faiss);remove_document();search(query_vector, top_k, min_score, doc_ids=None, exclude_template=False)→{"mode":"vector|empty","items":[{chunk_id,doc_id,seq,snippet,score,doc_name,doc_category,doc_tags,as_template,created_at}]}(分数低于 min_score 全部丢弃→empty,无降级档);rebuild_all();count()。单写锁;落盘 tmp + os.replace 原子切换(仿 retrieval.py 纪律)。
- 切块参数常量:
vstore/embed.py新增embed_batch_chunked(texts, batch_size=16, workers=4, url=None, model=None):Ollama /api/embed 的 input 数组批量(与现有 embed_batch 的单条循环区分),ThreadPoolExecutor workers 并发;单批失败重试一次仍失败该批降级零向量。保留 embed_batch 不动。测试注入钩子仿 set_embed_fn 模式。vstore/vault.py:__init__加载 ChunkIndex(路径来自 settings);add()在 doc 索引成功后追加:build_chunks → embed_batch_chunked → chunks.upsert_document;chunk 任何失败只告警不阻断,返回体追加chunk_count/chunk_ok/chunk_error;delete()连删 chunk 行;新增rebuild_chunks()(遍历密文解密 → build_chunks → 批量嵌入 → rebuild_all,返回 {docs, chunks, duration_ms})。vstore/api.py:GET /api/vstore/search-chunks?q&top_k&min_score&doc_ids&exclude_template(top_k clamp 1..20;内部asyncio.to_thread包同步段;响应{ok, mode, query, count, items});POST /api/vstore/rebuild-chunks;/health加chunk_count。ingest 端点不变(扩展走 vault.add 返回体)。vstore/settings.py:新增VSTORE_CHUNK_PATHenv(默认{root}/chunks)+ get_paths 暴露。- 新
vstore/scripts/rebuild_chunks.py:CLI 重建(cwd=部署根加载 vstore/.env,用 CLI 参数或环境变量取路径),进度打印 + 汇总;供 TL 后台执行。 - 新
vstore/tests/test_chunks.py由 T1 编写——你负责按 T1 用例自测通过。
P2 BFF 私有问答端点
sdk/app/schemas.py(白名单明文):PrivateQARequest(question, session_id=None, top_k=8, min_score=0.30);PrivateDocItem(doc_id/name/seq/chunk_id/snippet/score/category/tags/created_at/ref,全默认值 extra="allow");PrivateDocBlock(type: Literal["private_doc"], answer="", items=[]);PrivateDocBlock追加进Block = Union[...]末尾。sdk/app/services/docgen/client.py新增async def search_chunks(query, *, top_k=8, min_score=0.30, timeout=12.0) -> dict(GET /api/vstore/search-chunks;失败抛 DocgenUnavailable,同文件既有语义)。sdk/app/services/docgen/llm.py:complete_text增可选timeout: float | None = None(None 走 env 现行为;显式值覆盖),透传 _chat_once;docgen 既有调用零影响。- 新
sdk/app/services/privateqa.py(dataclass/dict,无 pydantic,无 yield):- 常量:
PRIVATE_QA_LLM_TIMEOUT=40.0、PRIVATE_QA_MAX_CTX_CHARS=12000、PRIVATE_QA_PER_DOC_MAX=3。 async def ask_private(question, *, top_k=8, min_score=0.30) -> dict:search_chunks → 空{hit:false, mode:"empty", answer:"", items:[]}→ 同 doc 按 score 序去重(每 doc≤3 块)再取 top_k → 上下文拼[资料n]《name》(相关度 x.xx)\nsnippet(12k 预算截断)→ complete_text(system=「只能依据资料回答,不得编造;每个关键论断标注 [资料n];资料不足则答『资料中未找到相关信息』;300 字内简体中文 markdown」, temperature=0.2, max_tokens=800, timeout=40) →{hit:true, mode:"vector", answer, items:[...带 ref], elapsed_ms}。异常抛 DocgenUnavailable/Exception 由路由转信封。
- 常量:
- 新
sdk/app/routers/private_docs.py:POST /api/qa/private(body=PrivateQARequest);命中{ok:true,hit:true,mode,answer,items,elapsed_ms};未命中{ok:true,hit:false,mode:"empty",answer:"",items:[]};vstore 不可达 503UPSTREAM_DOWN;LLM 失败 502UPSTREAM_ERROR(信封复用{ok:false,error:{code,message,degraded}})。 sdk/app/main.py:+1 行 include_router(放 docgen 之后)。
P3 前端接线 + 管理界面
- 新
ui/src/api/private.js:askPrivate({question, session_id, top_k=8, min_score=0.30})(POST /api/qa/private,timeout 60s)。 ui/src/stores/chat.js:send()内、_runStream/_runSearch 分发前插this._firePrivate(q, sessionId, aMsg)(不 await)。三个新方法按已批准设计:_firePrivate(fetch→未命中/异常静默 return;命中→本轮上下文校验→流中暂存aMsg._privatePending/ 终态直挂+补 persist)、_appendPrivate(防重入:已有 private_doc 块只挂一次)、_flushPrivate。flush 挂点:_runStream onComplete 整体替换 blocks 之后、_runSearch blocks 赋值之后、两条 error 路径。ui/src/components/chat/AssistantMessage.vue:新增private_doc块分支(渲染顺序放 sources 之后):卡体private-doc-block;标题「📚 私有资料参考」(i18n);answer markdown 消毒渲染;items 逐条(编号[资料n]、名称、相关度、片段)。- 新
ui/src/i18n/private.js:标题/相关度/管理界面文案(按 UI_SPEC §9.2 文案清单)。 - 新
ui/src/components/bench/PrivateLibraryPane.vue:组件内局部 state(listLibrary({}) 加载/删除后刷新/错误重试);删除 window.confirm → removeLibrary(id) → 刷新。挂 DocPane 空态区顶部(DocPane.vuev-if="!docgen.activeArtifact"块内第一个子元素,其余空态内容不动)。 ui/src/api/docgen.js新增removeLibrary(id)(DELETE;注意 204 空体——确认 http.js 对 204 的处理,异常时包装返回)。
自测要求(交付门槛)
- 本地跑通 vstore pytest(T1 用例全绿);python 语法/import 检查全部新文件。
- 前端
npm test相关用例全绿(T1 用例);npm run build无错误。 - BFF:schemas union 校验(用 TypeAdapter/MessageCreate 构造 private_doc 块通过);新模块无 yield(自查);python 语法检查。
- 交付物:代码改动清单 + 自测结果(粘贴测试输出摘要)回报 TL。不 commit、不部署。
参考(只读)
- 已批准实施方案(TL 提供,含全部签名与 JSON 契约)。
- 既有纪律与先例:sdk/app/services/sse_relay.py(原子写/帧格式)、vstore/retrieval.py(锁/rebuild 模式)、ui/src/stores/chat.js(sendDocgen 的 meta.library_ids 模式)、AssistantMessage.vue(sources 块分支写法)。