# Step3 知识图谱问答 Agent `src/step3_qa_agent/agent/` 负责问题理解、复用、接地、规划、执行审查和回答; `retrieval/` 负责受控 Neo4j 查询。答案必须来自实际检索结果,评估报告不能充当业务事实。 ## 每次提问的上下文流程 1. `understand` 每次执行时读取 `output/meta_graph_schema.json`,建立本轮快照。 问题理解提示词包含节点、属性、主键、合并/多值规则、关系方向/连接字段/判断方法和统计, 同时提供可用数据评估的目录;此时不放入评估正文。 2. 问题理解在原有 category/slots 之外返回: `data_needs: {datasets: [节点id], relations: [关系id]}`。 LLM 结合当前问题与历史对话选择所需数据;跨节点关系自动纳入两端数据集。 未知节点、未知或歧义关系会被排除并记录,不能扩大到全部评估。 3. 从 `data/production/analysis/qa_data_context.json` 中选取对应评估, 核对 source_sha256 与当前生产工作簿,以及字段集合与本轮元图谱是否一致。 只使用 Step1 精简评估;不会读取完整分析报告的原始数据,也不会临时重跑评估。 4. 将选中的字段含义、缺失率、数据粒度、候选键、查询用途及风险说明保存到 `AgentState.qa_context`。规划、能力拓展、复用判断、审查、反思和回答均读取同一快照。 提问间重新加载;同一轮重试或中断恢复期间不静默换版。 5. 回答历史记录 schema_build_id;当前图谱版本与历史回答不同或历史版本未知时, 不直接复用该历史答案,进入重新查询。 问题理解未返回有效 data_needs 时,仅以本轮问题和槽位做关键词兜底并记录该状态, 不继承上次选择。有效的空选择保持为空。闲聊也带本轮元图谱,但不注入业务评估正文。 ## 可用性与边界 - 元图谱缺失或无效:在调用 LLM 前明确报错,不能假装使用旧测试结构。 - 评估缺失、重复、字段不符、源文件过期或暂不可读:省略该评估,保留元图谱, 并在上下文中明确“缺少质量参考”,不解释为业务数据不存在。 - 提示词和日志使用评估摘要;不会把 source_sha256、原始工作簿内容或完整画像直接放入提示词。 - trace 中记录 schema_build_id、selected_datasets、loaded_datasets、 selection_source 和 assessment_issues,便于核对每轮实际使用的上下文。 - 数据评估只辅助解释和规划,不得覆盖图谱事实、编造工具、用源行数替代合并后的节点数。 - **本轮改动范围为上下文加载与路由**。旧固定查询工具仍需适配“人员信息/项目信息” 标签和多值属性;本轮没有重构检索工具、修改数据库、重生成评估或运行真实 LLM 问答。 - 本轮更新前已中断的旧任务没有 qa_context,应重新提问以建立新快照。 ## 主要文件 - `agent/question_context.py`:本轮快照、语义选择、关系端点补全和评估有效性校验。 - `agent/data_context.py`:评估摘要格式化,保留原有关键词调用兼容。 - `agent/schema_context.py`:优先从本轮状态组装提示词,保留旧独立调用入口。 - `agent/nodes.py` / `agent/state.py`:问题理解输出、状态传递及历史版本标记。 ## 验证 ```powershell uv run python scripts/test_qa_question_context.py uv run python scripts/test_data_analysis.py uv run python scripts/test_step2_production.py ``` 本轮10+3+13项本地测试通过;新测试使用模拟 LLM,并实际运行 LangGraph “问题理解→规划”的状态传递路径。覆盖多轮隔离、同轮快照、跨图谱版本复用、 选中数据集与关系端点、缺失/过期评估、闲聊及元图谱加载失败。 真实本地文件核验:人员选择仅加载人员评估;服务于关系加载人员与项目评估, hash和字段校验均通过。没有发送真实业务问题或评估内容到外部模型,尚不能据此 宣称真实问答准确率提高;需在查询工具适配后执行业务问题集验收。 ## 简单问题快速规划与中间输出(2026-09-07) 生产 Schema v2 在一次问题理解中同时提取可选的 `simple_query`。仅支持单数据集的 节点去重计数、属性查询和明细列表,最多6个明确文本等于/包含的 AND 条件。 候选必须声明完整、与原始 data_needs 单数据集/无关系选择一致,筛选值必须来自本轮问题, 已抽取的实体及筛选槽位不能遗漏,字段和结构必须通过现有只读编译器校验。 计数、关系或字段口径不明确时应返回 null。模型对完整性的判断仍需真实问题验收。 命中时程序构造计划,省去单独规划模型调用;仍经过中文计划确认、版本检查、 实际 Neo4j 查询、空结果候选及溯源回答。跨关系、时间/数值条件、分组排序、复杂表达、 历史指代、未知字段、缺失候选、修改反馈都回退完整规划。每次理解重置候选,旧会话不继承。 理解与完整规划均要求紧凑 JSON,不输出额外分析文字;省略空数组/空槽位、默认 limit=50、 重复 fields/depends。所有实际筛选、关联、分组、排序、返回字段和统计口径必须保留。 简单计数候选最小结构为: `{"kind":"count","dataset":"人员信息","complete":true}`。 最终答案仍按原有规则生成,不对 JSON 使用可能导致截断的硬性 token 上限。 可选环境变量(未设置时默认 true,支持 true/false/1/0): | 变量 | 作用 | |---|---| | `QA_FAST_PLAN_ENABLED` | 单数据集快速规划 | | `QA_COMPACT_INTERMEDIATE_ENABLED` | 理解与完整规划的紧凑输出 | 两个都设为 false 可恢复原理解/规划提示词及两次调用路径。修改配置或代码后重启服务, 无需重新取数或构图。 `api_log` 中 state.trace 的 understand/production_plan 新增 duration_ms; production_plan.mode 为 simple 或 llm,reason 说明命中或回退原因,validation_attempts 记录完整计划结构校验次数。llm_calls 每项记录一个 SDK 流的耗时、首次 chunk/文本等待、 输入/输出字符数及可用的 usage。JSON 解析重试计为不同流;SDK 内部网络重试只能计入耗时, 不能据此声称记录了精确网络请求次数。usage 缺失为 null,不把字符数当 token 数。 耗时不包含人工等待;节点耗时减去流耗时包含上下文、SDK创建等本地处理。 离线回归: `uv run --frozen python -X utf8 -m unittest scripts.test_simple_plan scripts.test_qa_question_context scripts.test_value_clarification scripts.test_plan_presentation scripts.test_runtime_workflow -q`。 真实测试脚本 `scripts/benchmark_qa_planning.py` 交替关闭/开启两个优化,记录理解和规划耗时; 它会调用真实配置的外部模型,须取得授权后运行。加 `--execute` 执行受控只读图谱查询, 仅保存结果行数、截断标记及按字段/操作归一的结果哈希,不保存原始查询行。 输出默认在 `.runtime/qa/planning-benchmark.json`。哈希比较需确认同一 build_id、 同一口径且结果未截断;随机模型输出和无排序明细可能不同。单次样本不能代表 P95。 ## 用户确认的独立问答缓存(2026-09-07) 网页版完成生产图谱回答后,在答案下显示“答案正确,加入缓存”。只有点击此按钮, 服务端才把该答案加入独立 Redis 索引;查询计划的“确认”或自动确认不会写入缓存。 保存成功后可“撤销缓存”,缓存命中显示“已复用你确认的历史答案”。 索引使用 `ka:qa:approved:v1:` 前缀的 Redis String,与上下文 checkpoint 分离。 键包含问题文本哈希、thread_id 隔离范围、最近对话摘要哈希、数据版本、Schema版本、 模型名及缓存策略版本。只去除问题两端空白,不去除否定、数字、标点或实体内部空格; 不做相似度直接复用。新提问先作一次有超时限制的 GET;未命中继续原问答流程。 同一问题连续重复时沿用原问题的上下文签名;中途问过其他问题会重新计算签名, 因此可能不命中。带明确历史指代或相对时间的问题保守地不缓存。 命中后直接复用用户确认的答案、查询计划和来源,跳过模型及图谱查询, 但仍追加本轮历史并保存新的 checkpoint;关闭页面“历史复用”可强制正常问答。 目前API没有真实鉴权,缓存仅按会话隔离,清空/新建会话不会共享已有缓存。 未来接入鉴权后须把实际用户/租户/权限版本纳入键,不能使用当前固定 user_id=api 共享。 仅接收答案ID及checkpoint ID,不接收浏览器提交的答案正文。服务端读取该会话不可变 checkpoint并校验完成状态,所以旧答案按钮也不会误确认后来的另一条回答。 过期checkpoint无法确认。错误、闲聊、不支持、截断/缺失结果、待澄清、 修改过计划/替换过候选、依赖上下文/相对时间及超过256KB的记录不入缓存。 保存查询结果与来源仅用于复用已确认内容,不把评估报告当作事实。 缓存永久保存(不设置TTL),数据版本发布成功后删除全部旧数据版本缓存。写入通过Redis原子脚本核对当前数据版本,不设置过期时间。数据或Schema更新、模型名、 策略版本变化均不再命中旧键。数据版本发布成功后,仅扫描独立问答索引并删除全部非当前数据版本记录,不影响会话上下文及其他Redis数据。清理失败记录警告,服务启动及运行期间每60秒重试;启动时将尚存的当前版本旧TTL记录转为永久。 撤销使用比较后删除,避免并发时删除另一条新确认记录。 缓存读超时/损坏时回退正常问答;写入/撤销失败会明确提示,不显示成功。 底层Redis整体不可用仍会影响原有checkpoint持久化,不承诺整个服务脱离Redis运行。 | 配置 | 默认 | 用途 | |---|---|---| | `QA_ANSWER_CACHE_ENABLED` | true | 独立答案缓存及确认入口开关 | | `QA_ANSWER_CACHE_TTL_SECONDS` | 已停用 | 旧配置被忽略;问答缓存永久保存 | | `QA_ANSWER_CACHE_POLICY_VERSION` | 1 | 提示词/权限/回答策略变化时主动递增以失效旧缓存 | 修改代码或配置后重启服务,无需取数或构图。现有缓存不会自动认定旧上下文中的答案正确, 历史答案必须由用户通过对应按钮确认。 主要实现:`src/step4_web/answer_cache.py`、`api.py`、 `html/answer_cache.js`、`html/answer_cache.css`;根页iframe版本同步更新。 离线验证: `uv run --frozen python -X utf8 -m unittest scripts.test_answer_cache -q`; 页面:`node scripts/test_answer_cache_ui.cjs`(需Playwright和Edge)。