fix(agent): reuse collected vehicle counts and avoid repeated questions

This commit is contained in:
Eric Wang
2026-10-28 11:30:56 +08:00
parent a277d4c7e6
commit 20c5e39e9d
15 changed files with 7073 additions and 47 deletions
+104
View File
@@ -0,0 +1,104 @@
# 客户反馈:单方事故数量与重复追问
## 最新业务边界:数量先看表单
用户已确认“倒车撞护栏”按已知1辆机动车处理;泛化到完整描述自身驾车撞固定物、无其他车辆参与线索的单方事故,不要求额外报车型或说“只有一辆”。采集节点先读取 `form.vehicle_count`,有效值没有新更正或冲突时直接使用,不再询问数字,也不再确认是否只有一辆。历史窗口缺少原始回答不使有效表单值失效。其他信息仍缺失时只补问其他信息。
本次同步采集/提取提示词和本仓库工作流JSON。两个历史名为 `single_no_inferred_count` 的YAML案例根据用户新边界更新:原始事故描述不改,新增 `vehicle_count: 1` 断言,时间确认后预期直接进入2000,删除额外要求用户说“只有一辆”的轮次;其余44份旧案例保持不变。该预期修改来自用户明确确定的业务边界。
语义检查补充了倒车撞其他固定物、表单已有1/2/3辆、数量已知但人伤缺失等情况。历史相对时间无基准的问题按用户要求暂缓,运行时规则和该案例保留;本次通过 `--exclude-case missing_historical_time_anchor` 显式排除,排除项单独写入结果,不算通过。
下文“验证记录”保留上一轮边界确定前的历史实测;本次实测另见文末更新。
本轮以客户“自己的小轿车撞到路边围栏”的对话为依据,改进事故数量提取和已有信息复用。采用 `.agents/skills/` 下的 `prompt-debugging`、`prompt-engineer`、`promptfoo-evaluation`,以及用户要求参考的 `skill-creator`;复用现有 Python 评估器,不另建一套 Promptfoo 执行链。
## 原因
1. 原采集提示词将固定物碰撞与机动车数量严格分开,缺少对完整单方事故语义的认可,导致车型、经过都已清楚仍追问数字。采集和提取两个独立模型调用还可能使用不同口径。
2. 用户质疑重复提问时,旧规则倾向于维持上一条问题,而没有先重新读取历史与当前表单,容易给出“没有看到您之前的回答”的机械回复。
3. LangGraph 采集用户消息只有参考时间和用户输入;表单只出现在额外的系统会话上下文。工作流 JSON 的采集用户消息也没有 form。
4. 连续呼叫只按闲聊处理,完整事故经过模板被反复附在回复后。
## 改动与边界
- 采集、提取共享单方事故语义:完整描述自身驾车撞固定物、无其他车辆参与线索或冲突时,机动车数量为1;包括倒车撞固定物而未报具体车型的经过。明确自身为非机动车的同类场景为0。不要求额外说“只有一辆”或“单方”。
- 只报车型、只提固定物或碰撞关系不清,不由此补数量。存在其他参与车辆线索但范围不清时继续澄清。用户最新明确更正优先。
- 每轮先合并 form、用户历史与本轮事实,再查缺。表单中的false、0不当作缺失;表单时间不代表已经获得用户确认。
- 数量已知就采用,不再换成“是否只有一辆”重复确认。用户抱怨时承接已有信息,继续缺项或执行分流,不断言用户此前没有回答。
- 连续呼叫时简短回应在线并帮助恢复交流,避免重复事故模板;仅文本输入不声称实际听到了用户。
- `_build_user_message` 在 `<turn_context>` 内增加 JSON 编码的 `form`,与当前 `accident_fields` 快照一致。LangGraph 在已有部分表单时同时提供 `form_unfilled_fields`,按既有采集顺序显式列出表单缺键、null或空字符串的字段;false与0不列为缺失。空表单和完整表单不额外提供该列表,避免影响从历史恢复信息。该列表仅描述表单,模型仍需从历史与本轮表达补足事实。代码不按关键词推断数量,不代替模型决定分流;裸提示词probe复用同一用户消息构造函数。
- 本仓库新增 `workflow/20261027/事故信息采集2025118.json`,基于用户指定目录的 JSON,仅更新两个提示词和采集 `userChatInput`。外部 `C:/Users/52943/Code/ZNJJ-api-server` 工作区不修改。逐字同步测试改用此快照。
安全优先、明确人伤立即转人工、时间独立确认、状态码与拍照可执行性保持原规则。
## 评估设计
实际配置模型为 `deepseek-v4.1-flash`,temperature为0,`extra_body`为 `{"reasoning_effort":"none"}`。API凭据不写入报告。
- 第一轮保留原有46个YAML案例及断言;用户明确新边界后,仅更新两个历史单车数量案例,其余44份不变。
- 新增4个YAML案例,覆盖direct和browser_addon两种模式:客户完整对话在说明仍在现场后直接进入单车拍照;提前提供所有字段并伴随重复提问抱怨时不重问已知项。
- 原对话尾部的错误数量问题和客户质疑,使用 `evals/prompt_probes.py` 的历史回放验证修复后的恢复行为;正常端到端会话应在错误问题出现前进入拍照,因此不强行让正常会话继续走旧错误路径。
- 第一轮补充26项语义检查;最新数量迭代扩展为30项,暂缓1项历史时间基准检查,执行其余29项。覆盖原始表达、不同车型和固定物、仅车型不足、其他车辆参与不明、历史已有数量、只有表单有1/2/3辆、数量已知而其他字段缺失、false保留、表单时间未确认、数量更正、连续呼叫、无历史证据,以及既有安全边界。
- 实测使用真实模型、生产采集函数与 `/chat` 路由,不用FakeLLM替代。评估存储覆盖为memory,避免接触当前SQLite会话;评估结果保存在被Git忽略的 `evals/results/customer-feedback/`。
## 复现
```bash
conda run -n py3.11 python -m evals.prompt_compare --output-dir evals/results/customer-feedback/reproduce-all
conda run -n py3.11 python -m evals.prompt_probes --runtime --exclude-case missing_historical_time_anchor --output evals/results/customer-feedback/reproduce-probes.json
conda run -n py3.11 python -m pytest -q
```
使用独立评估存储时,运行前设置 `AGENT_STORAGE=memory`。新案例的表单事故时间按既有API规范断言为“14点0分”;回复的独立时间确认仍断言完整“14点00分”,不修改存储规范化逻辑。
## 验证记录(上一轮边界确定前)
修改前原有46项端到端案例为46/46。中间版本v5为50/50(原46项与新增4项全部通过);后续迭代观察到两个旧的 `single_no_inferred_count` 案例存在波动:其描述没有明确车型,但提取器可能先写入1,随后采集节点采用表单值而提前进入拍照。旧案例输入与断言均未修改,不能把这些波动当作已解决。
本轮优先保证完整单方机动车描述无需重复数量确认,并保留时间确认要求。最终版语义检查为25/26,客户原始描述、回放错误数量问题后的恢复、异车型/固定物、真实参与范围歧义、只有表单含数量、false保留、数量更正和表单时间未确认均通过。唯一失败为历史相对时间没有可靠基准时,运行时校验抛出 `CollectReplyError`;这项是已有时间边界,未将被拒绝的回复算作合格澄清。
新增端到端案例检查呼叫后在线回应及不虚构听觉能力;完整事故模板重复的风格检查单独放在语义probe中,避免这项附加风格目标阻止客户数量问题的完整对话验证。未放宽数量、表单、时间确认或状态断言。
全量单元测试为338通过、13跳过;最终提示词/JSON同步与评估器相关测试为26通过。JSON结构比对确认相对用户指定原文件只有两个提示词输入与采集userChatInput三处变化,其他数据相同。
最终端到端统计:49/50。其中原有案例45/46,新增客户案例4/4。剩余失败是 `browser_addon_collection_skip_known_info_single_no_inferred_count`,时间确认后模型采用已被提取为1的表单数量而进入拍照,旧断言要求继续澄清数量。该场景没有明确车型,与本次客户“小轿车撞固定物”的明确单方描述不同,仍保留为未解决的提取边界;不删除用例、不改为接受拍照。不同迭代中同一场景曾通过,因此不能仅凭一次通过宣称稳定。
| 验证项 | 基线 | 最终版 |
| --- | --- | --- |
| 原有端到端案例(46份YAML的SHA-256一致) | 46/46 | 45/46 |
| 新增客户反馈端到端案例 | 未纳入原46项基线 | 4/4 |
| 补充语义检查 | 18/26 | 25/26 |
| 全量单元测试 | — | 338通过、13跳过 |
| 最终同步及评估器相关测试 | — | 26通过 |
原始结果:`evals/results/customer-feedback/final-v7b-all/` 与 `final-v7-probes.json`;汇总为 `comparison.json`。逐例审核页为 `evals/results/customer-feedback/review.html`,使用 `skill-creator/eval-viewer/generate_review.py` 生成,展示原提示词与最终提示词的实际回复、字段和断言。评估产物沿用Git忽略规则,不随仓库提交。
各次中间失败均保留在原始运行目录,不与最终结果混合统计。单次通过率只代表本次样本,不能证明所有转写与模型输出都稳定。补充基线使用原提示词和同一新版运行时上下文,用于比较提示词行为;原46项基线在修改采集用户消息前运行。
## 数量边界确认后的最终验证
本次采用用户明确的新边界,采集与提取均将完整倒车撞固定物按单方机动车数量1处理,表单已有有效数量且无新更正/冲突时直接采用。未填字段提示仅在部分表单时提供,并按采集顺序排列;数量已知、人伤仍未知时继续1002询问人伤,不能提前拍照。时间规则和历史时间基准案例未修改,后一项显式暂缓。
使用真实 `deepseek-v4.1-flash`、temperature=0:
| 验证项 | 本次最终结果 |
| --- | --- |
| 全部端到端案例 | 50/50 |
| 数量及通用语义检查 | 29/29 |
| 历史相对时间缺基准检查 | 1项暂缓,不计为通过 |
| 提示词同步、上下文和评估器相关单元测试 | 26通过 |
原有46份YAML中,44份SHA-256保持不变;两个历史名为 `single_no_inferred_count` 的案例按用户确认边界更新,并增强为检查表单数量1及禁止再次询问数量/车型。原始倒车事故描述保持不变。新增4份客户反馈案例继续通过。
专项覆盖:表单只有数量记录而历史未展示原回答、表单已有1/2/3辆、已有数量但其他字段缺失、当前明确更正数量、完整倒车撞其他固定物、真实其他车辆参与不明。模型在“数量1、人伤未填”的案例中保留数量,正确询问人伤;不再把减少数量追问变成跳过其他必要字段。
本次全量单元测试首跑337通过、13跳过、1失败,失败为 `test_sqlite_disconnect_after_reply_finishes_preserves_committed_snapshot` 的SQLite中断事务时序断言;单项复跑通过(1 passed)。未修改存储实现,也未把单项复跑说成整套一次通过。相关26项测试在最终修改后通过,`git diff --check`通过。
原始结果:`evals/results/customer-feedback/quantity-ordered-all/`、`quantity-ordered-probes.json`;汇总:`quantity-summary.json`;审核页:`quantity-review.html`。全量单测首跑日志为 `quantity-unit-tests.log`。中间失败和被交互中断的未完成运行保留,不并入最终通过数。审核页由skill-creator自带生成器制作,展示本轮29项检查的真实回复、表单与断言。
复现本次数量边界检查:
```bash
conda run -n py3.11 python -m evals.prompt_compare --output-dir evals/results/customer-feedback/quantity-reproduce-all
conda run -n py3.11 python -m evals.prompt_probes --runtime --exclude-case missing_historical_time_anchor --output evals/results/customer-feedback/quantity-reproduce-probes.json
```