diff --git a/data/business.db b/data/business.db index f909e99..8ae7ecf 100644 Binary files a/data/business.db and b/data/business.db differ diff --git a/kb/consts/consts.go b/kb/consts/consts.go index b77b662..80d1868 100644 --- a/kb/consts/consts.go +++ b/kb/consts/consts.go @@ -28,8 +28,8 @@ const ( ParsePollIntervalSeconds = 15 // 解析/标注任务轮询间隔 - DefaultLlmHttpTimeout = 600 // LLM HTTP 客户端超时(秒):本地模型生成慢,且多请求排队时响应可超分钟级 - MaxExtractTokens = 4096 // 知识抽取单次生成的 token 预算(防失控生成烧掉上下文) + DefaultLlmHttpTimeout = 600 // LLM HTTP 客户端超时(秒):本地模型生成慢,且多请求排队时响应可超分钟级 + MaxExtractTokens = 24576 // 知识抽取单次生成预算(思考链+JSON;24576≈实测自然输出 20 倍,4 并发下不超 131072 共享上下文) EmbedBatchSize = 16 // 单次向量化请求的文本批量 @@ -38,10 +38,11 @@ const ( AnnoMaxCandidates = 60 // 多数据集融合后的候选上限(喂给 LLM 判定) AnnoMaxClauseChars = 2000 // 合同条款全文上限(超长截断,控制 prompt) - // 判定 prompt 上下文预算(本地 4B 模型 context 8192 token,须留生成余量) - AnnoJudgeMaxTokens = 1024 // 判定输出 token 上限(JSON 结果含 ≤3 条风险;须显式限制,防推理模型长思考烧光上下文) - AnnoJudgeCandidateChars = 300 // 判定 prompt 单候选条文截断字数(精确条文文本由 ContentFull 抽取,不依赖 prompt 全文) - AnnoJudgePromptBudget = 1500 // 判定 prompt 候选块总字数预算(候选按 RRF 相关度降序贪心填充,超预算截断后续候选) + // 生成预算=思考链+JSON:实测判定自然输出仅 ~1.1k token,24576 留 20 倍余量(截断物理上不可能); + // 且 4 并发×(prompt≤3k+24.5k)≈110k ≤ 131072 共享上下文,更高值(如 32768)会触发并发拒收 + AnnoJudgeMaxTokens = 24576 // 判定输出 token 上限(JSON 结果含 ≤3 条风险) + AnnoJudgeCandidateChars = 300 // 判定 prompt 单候选条文截断字数(精确条文文本由 ContentFull 抽取,不依赖 prompt 全文) + AnnoJudgePromptBudget = 1500 // 判定 prompt 候选块总字数预算(候选按 RRF 相关度降序贪心填充,超预算截断后续候选) // 合同风险识别 RiskLevelHigh = "high" // 高风险(违反强制性规定、可能导致合同无效/赔偿) diff --git a/技术设计.md b/技术设计.md index 81c7c74..42fa00b 100644 --- a/技术设计.md +++ b/技术设计.md @@ -292,7 +292,7 @@ StartParsePoller(main.go 启动,gtimer 单例 5 秒轮询,job 未结束不 - **条款切分**:按优先级探测三种行首正则(`第X条` / `\d+(.\d+)*[、..]` / `中文数字[、..]`),命中 ≥2 采用,否则整篇单条(title=「全文」);title=标记、content=标记行至下一标记全文 - **召回**:每 dataset 各调 `VecSearch(dsId, vec, 15)` + `FtsSearch(dsId, 分词截 200 字, 15)`,全局按 RRF(`1/(RrfK+rank+1)`) 融合截 `AnnoMaxCandidates=60`;候选 chunk 内容**批量加载**(`ListByIds` 一次 IN 查回内存映射,禁止逐条 GetOne——N+1);embedder 按 dataset 绑定的 embedding 配置构建并缓存;**无候选的条款视为完成**(跳过 LLM 调用,无 mark) - **判定**:单次 LLM 调用,prompt 含条款全文(截 `AnnoMaxClauseChars=2000`)+ 编号候选,输出 `{"marks":[{"cand_id":1,"law_item":"第四十四条","score":9,"reason":"..."}]}`;**law_item 由 LLM 判定输出**(法条编号,`第X条` 正则兜底提取);JSON 解析与 rerankByLLM 同套路(```json 提取 + 截首尾花括号);**不做门槛过滤,全部候选(含 0 分)按分降序保留** -- **判定 prompt 上下文预算**:本地 gemma-4-E4B 由 LocalAI 托管(`models/gemma-4-E4B.yaml`,llama.cpp 后端 gRPC 上报错 `rpc error`),`context_size=16384` **跨 4 个 parallel slot 共享**——上限按并发请求合计占用量算,单发请求可用 ~16384,4 条并发判定(每条 prompt ~1500 + 输出 1024 ≈ 2657 token)合计 ~10k 也会超出(曾以 8192 运行,4 并发全量被拒 `Context size has been exceeded`;提至 16384 后单发余量充足)。两道闸(缺一不可):① **服务端关闭思维链**——gemma-4-E4B 是推理模型,默认输出长思考链且写入 `reasoning` 字段,烧光 `max_tokens` 预算导致 `content` 为空或 JSON 截断(`thinking:false` 只削弱不消除,实测仍占 2.7k+ 字符;`models/gemma-4-E4B.yaml` 配 `reasoning_effort: none` 才能完全关闭,作用于所有请求,应用侧 `DisableThinking()` 的 `thinking:false` 保留为通用兜底);② **显式 `max_tokens=AnnoJudgeMaxTokens=1024`**;③ 候选块按 `AnnoJudgePromptBudget=1500` 字总预算**贪心填充**(按 RRF 相关度降序,每条截 `AnnoJudgeCandidateChars=300` 字,首条保底入队,超预算截断后续候选)。条文精确文本不依赖 prompt 全文——`extractLawItem` 用未截断的 `ContentFull` 抽取;LLM 引用编号受展示条数约束(越界引用丢弃) +- **判定 prompt 上下文预算**:本地 gemma-4-E4B 由 LocalAI 托管(`models/gemma-4-E4B.yaml`,llama.cpp 后端 gRPC 上报错 `rpc error`),`context_size=131072`(模型上限,GGUF `gemma4.context_length`;2026-08-11 起)**跨 4 个 parallel slot 共享**——上限按并发请求合计占用量算(曾以 16384 运行,4 并发全量被拒 `Context size has been exceeded`)。**思维链默认开启**(推理模型,2026-08-11 起按用户要求移除 `reasoning_effort: none`,思考内容写入 `reasoning` 字段;此前靠它+`thinking:false` 双关仍占 2.7k+ 字符)。**生成预算 `max_tokens=24576`**(判定与知识抽取同值,2026-08-11 起;此前判定 1024/抽取 4096,思考链会烧光预算导致 `content` 为空或 JSON 截断):实测 4 并发判定自然输出仅 ~900-1100 token(思考链 ~3k 字符),24576 为 ~20 倍余量、截断物理上不可能;并发核验 4×(prompt≤3k+24.5k)≈110k≤131072 且实测 4 并发全部通过,更高值(如 32768)在真实 prompt 下会触发共享上下文拒收。候选块按 `AnnoJudgePromptBudget=1500` 字总预算**贪心填充**(按 RRF 相关度降序,每条截 `AnnoJudgeCandidateChars=300` 字,首条保底入队,超预算截断后续候选)。条文精确文本不依赖 prompt 全文——`extractLawItem` 用未截断的 `ContentFull` 抽取;LLM 引用编号受展示条数约束(越界引用丢弃) - **快照落库**:mark 存命中 chunk 的法条快照(law_title=dataset 名、law_item=LLM 判定的法条编号、content=chunk 内容截断 800 字),标注结果不随语料变更失效 - **批量落库(禁逐条 SQL)**:条款插入与风险快照用 `Batch(100)` 多行 INSERT(GoFrame 一次语句写 100 行,8 列 ≈800 变量 < SQLite 999 上限);进行中/完成状态用 `UpdateStatuses`(WHERE id IN,≤100 分批)各刷一次;进度按**本地计数**每完成一条刷一次 `UpdateProgress`(不再逐条款 `ListByTask` 读库统计)。仅失败路径保留逐条 `UpdateStatus`(error_msg 各异的罕见路径,不批量) - **来源文件标注**:法条引用快照同时记录源文件名(`LawRef.source_file`,如「劳动合同法.pdf」)——law_title 只是 dataset 名(如「法律」),看不出出自哪部法文件,溯源到 chunk 才能定位。判定时按单表约束拆两条 SQL(`kb_chunk` 按 id 批量查 document_id、`kb_document` 按 id 批量查 filename,IN ≤100 分批)内存组装;界面「法律依据」与导出 HTML 显示「来源:xx.pdf」;溯源失败仅告警不阻断标注(展示增强,非判定依据)