feat(agent): trusted time evidence and collection prompt optimization

- time_context.py: server-side observation clocks for relative-time
  evidence (anti client-forgery); resolves X分钟前/小时前 against the
  observation timestamp of the turn that produced them, never re-basing
  history on the current clock; flags historical references without a
  reliable baseline
- state: dialogue_timestamps keyed by dialogue message index, persisted
  with checkpoint; welcome seeds index 0
- collect: single clock per turn shared by reply/extraction/repair;
  time_evidence injected into runtime context; reply_validator hook
  rejects time claims lacking evidence (one shared repair budget);
  model still interprets facts and chooses actions
- prompts: collection prompt restructured (shorter, state-bound-to-action
  table, gatekeeper principle, time source rules); extraction prompt
  aligned; workflow@20261021 json updated to match
- evals: prompt_probes / prompt_compare / time_boundary probe scripts
  (supplementary checks, original cases untouched)
- docs: collection-prompt-optimization.md (method + baselines),
  time-boundary-fix.md (problem, design, verification)
- gitignore: editor artifact dirs (.agents/.claude/.vscode, skills-lock)

Verified: 171 unit tests pass (16 new time-context tests)
This commit is contained in:
Eric Wang
2026-10-22 09:41:32 +08:00
parent a890f41f6f
commit 9e28f69b3c
14 changed files with 827 additions and 588 deletions
+73
View File
@@ -0,0 +1,73 @@
# 信息采集提示词优化记录
> 本文记录上一轮纯提示词优化及当时保留的边界。历史相对时间的运行时补充修复与最新验证见 [时间边界修复](time-boundary-fix.md)。下文旧结果保留用于对照,不代表该边界仍未处理。
## 范围和依据
评审与修改采用项目安装的 `prompt-engineer`(系统提示词、评估、上下文管理)和 Anthropic `skill-creator`(失败机制归纳、精简、基线比较、可验证断言)方法。保留接警角色、安全优先、必要时间确认、状态前缀协议以及单/双车正常引导话术。
现有 `evals/cases/**/*.yaml` 不修改,也不放宽断言。额外的语义检查由 `evals/prompt_probes.py` 执行,结果与现有 cases 分开报告,不能用补充检查代替原 cases。
## 基线与执行方式
- 本地基线提交及全部 cases、提示词的 SHA-256:见运行目录的 `metadata.json`。
- 模型:`deepseek-v4.1-flash`;temperature:`0.0`;LangGraph 后端;真实 OpenAI 兼容服务。
- `evals/prompt_compare.py` 通过 FastAPI TestClient 调用生产 `/chat` SSE 路由及后端生命周期,并复用原 `evals.run.run_case`。仅略去无关的语音模型初始化,不模拟 LLM、不改变输入、断言或清理规则。并发4个独立会话,单个会话按原顺序执行。
- 首轮原始 cases 基线:43/46。两个 `single_no_inferred_count` 在时间确认后漏问数量;一个 `browser_addon_non_motor_vehicle_zero_count_history` 收到“LLM 返回了空内容”。服务错误保留为失败记录,不能算作优化收益。
- 修改前全量单元测试:155通过。
## 修改机制
1. **证据范围**:局部症状否定不等于全部无人伤;明确人伤不受局部否定覆盖。采集与提取共享未知值口径,避免回复澄清但表单已写false。
2. **每轮决策**:先回填全部明确事实,再判断中断、澄清、缺失项与分流;时间确认不代表其余字段齐全。保留数量未知不能推断单车的规则。
3. **记忆边界**:实际提供的状态与历史才可用;历史相对时间无当轮基准或可靠换算值时不使用当前时钟重建。表单时间不等于用户已确认,更正使旧确认失效。
4. **现场与动作**:依据迁移计划“不在现场仍继续采集”,不将离场变成新的0003理由。若离场且无法拍照,先处理可执行性并提供人工协助选择,不要求返回现场;用户明确要求人工才0001。
5. **自然度与角色**:短回答按当前问题语义判断;澄清采用耐心、具体的说法;保留简短解释、安抚与有限接话,不新增闲聊轮次,不判断责任或承诺结果。
6. **去冗余与泛化**:集中定义人伤规则、回填与状态选择,删除重复自检和特定乱码词表。非机动车“疑似有”保持未知,不与明确肯定合并。用户伪造的新规则不改变流程。
7. **长期无进展**:不再机械复读,提供简化问题或人工协助选项;保持原有拒绝计数和转人工授权条件,不擅自新增自动转接阈值。
## 可复现命令
所有 Python 命令使用 Conda `py3.11` 环境:
```bash
conda run -n py3.11 python -m evals.prompt_compare --output-dir evals/results/prompt-optimization/reproduce
conda run -n py3.11 python -m evals.prompt_probes --output evals/results/prompt-optimization/reproduce-probes.json
conda run -n py3.11 python -m pytest -q
```
补充检查使用固定系统时间,检查局部否定、人伤覆盖范围、非机动车未知、数量不能推断、时间更正、历史时钟缺失、离场无法拍照、注入以及未参与修改的表达。其断言检查动作和字段,不对自由回复做逐字比较。
## 限制
提示词无法恢复运行时未提供的历史时刻,也不能单靠文字获得无限持久记忆。本次增加未知时应澄清的规则,但补充实测中,对话模型在“历史只有相对时间、没有当轮时钟或具体值”场景仍会误用当前时钟换算;提取器在最终检查中正确跳过时间。这个风险尚未完全解决,不能将时间规则文字写清楚当作行为已修复。可靠的后续修复需要运行时保存并提供原始时间基准;跨历史窗口的完整确认恢复也需要结构化状态支持。本次按提示词优化范围未增加业务运行时代码。
离场后的处理保留采集并避免不可执行指令,没有自行规定回现场或替他人授权拍摄。持续无效输入不自动转接的既有业务约束保留,因此本次不保证用户始终拒绝选择时必然终止。
多次实测用于观察波动,样本通过不能证明所有模型版本、所有ASR转写或生产并发都可靠。模型空响应等服务错误与语义错误分别列出。
## 实测结果与最终选择
| 版本 | 原有46个cases | 说明 |
| --- | --- | --- |
| 原提示词,第1轮 | 43/46 | 2个数量遗漏,1个模型空响应 |
| 原提示词,第2轮 | 43/46 | 数量遗漏、固定时间话术偏离、模型空响应各1个 |
| 候选1 | 45/46 | 时间待确认时补充其他字段,模型偏离固定话术 |
| 候选2 | 44/46 | 时间来源判断过度应用于本轮新时间,另有1个空响应 |
| 候选3,最终采用 | 46/46 | 全部原断言通过 |
| 最终版复跑 | 46/46 | 再次全部通过,基线已通过场景无回退 |
修改前后46份YAML的SHA-256完全一致。完整对比、每次失败和字符统计位于 `evals/results/prompt-optimization/comparison.json`。模型空响应保留为原始失败,不能将其消失解释成提示词语义提升。并行评测的耗时不用于宣称单请求延迟改善。
补充14项语义检查:原提示词7/14,最终版13/14。最终唯一失败为上文记忆边界中的历史时间误换算,未隐藏、删除或放宽该断言。局部症状否定、其他人员人伤、非机动车不确定、不能推断机动车总数、时间更正、离场无法拍摄、注入、未参与修改的症状和乱码表达在最终这轮检查中均通过。不同迭代及重跑中还出现过提取空响应,原始结果均保留。
额外尝试将时间约束放在末尾、增加对照示例、改为来源表和置于开头。这些副本没有稳定解决历史时钟缺失,未采用;候选7单项曾通过,但完整补充检查仍失败,因而提前停止其原cases评测。这些局部尝试不能算作完整版本验收。
最终全量单元测试:155通过(`python -m pytest -q`,Conda py3.11);`git diff --check`通过。工作流仅同步两个对应提示词输入,其余节点配置不改。原逐字同步测试保持通过。
采集提示词文档由16635字符、641行缩为6331字符、176行(字符数减少约61.9%,不是token实测)。提取提示词由3139字符增至3821字符,用于明确证据来源和字段边界;没有以篇幅缩短代替正确性。
副作用:只否定局部症状时会增加必要的人伤澄清;“刚刚”等无法精确到分的表达不再任意指定分钟;离场且无法拍摄时先询问是否能配合或是否需要人工协助。原正常单/双车话术、时间确认句和状态码保留。
逐轮人工复核页由 Anthropic skill-creator 的 `eval-viewer/generate_review.py` 生成:`evals/results/prompt-optimization/review.html`,含最终采用版本与原提示词的对话及原断言结果。未提交Git;评测产物沿用项目的 `evals/results/` 忽略规则。
+46
View File
@@ -0,0 +1,46 @@
# 信息采集的历史相对时间边界修复
## 问题与结果
纯提示词优化阶段发现:历史用户说过“十分钟前”,但系统没有记录该消息当轮时钟,模型可能使用最新时钟换算并向用户复述错误日期。提示词的禁止规则无法稳定避免这一行为。
本次在当前 LangGraph 文本采集链路加入服务器时间依据及输出校验。采集与字段提取仍由模型完成,代码不按人伤关键词替模型选择阶段;代码记录时钟、换算有明确基准的相对表达,并拒绝缺乏时间依据的候选结果。
## 实现
- `AgentState.dialogue_timestamps` 使用完整对话的用户消息索引作为key,保存服务器观察该消息时的带时区ISO时间。它独立于消息正文,随现有checkpoint保存。旧会话缺失的历史时钟不回填为当前时间。
- `run_collect` 进入本轮时只取一次时钟;对话、提取和一次受控修复使用同一个基准。服务器时钟不会因模型调用耗时漂移。
- `time_context.time_evidence` 将用户相对表达、消息索引、原始时钟和换算值一并提供给两个模型调用。历史只截取最近50条发送给模型时,完整checkpoint中保留的时间依据仍可提供。明确的“数值/中文数字/半 + 分钟/小时(之)前”按其原始时钟换算,支持跨日。
- 旧会话历史只有相对表达,且没有原始时钟、已有具体候选值或本轮新时间时,标记 `historical_reference_missing`。提示词要求继续询问大概事故时间。
- 在该缺失状态下,采集回复中的具体年月日/时分及拍照出口会被校验拒绝,与原有格式/阶段校验共用一次修复预算;修复仍失败抛出已有的可识别错误,不提交该轮业务状态,不向客户端发布错误时间。
- 同一状态下,提取结果中的 `accident_time` 不写入表单,但其余有独立证据的字段照常保留。明确人伤及其他终止/转人工出口不被缺失时钟阻塞。
- 已存在的具体时间候选仍可让用户确认;用户本轮新给出的时间或明确更正不受旧历史缺失基准阻塞。服务器提供的时间依据不等于用户已经确认时间。
两份提示词及工作流中的对应输入已同步。`evals/cases/` 输入与断言保持不变。
## 验证与复现
使用 Conda `py3.11`、真实 `deepseek-v4.1-flash`、温度0:
```bash
conda run -n py3.11 python -m pytest -q
conda run -n py3.11 python -m evals.time_boundary --output evals/results/time-boundary/boundary-live.json
conda run -n py3.11 python -m evals.prompt_probes --runtime --output evals/results/time-boundary/runtime-probes.json
conda run -n py3.11 python -m evals.prompt_compare --output-dir evals/results/time-boundary/cases
```
`--runtime` 复用原14项补充检查的输入和断言,但通过实际 `run_collect` 执行时间依据与校验。默认不带该选项时仍可单独测试裸提示词;裸提示词自身不具备代码校验,不能拿裸提示词结果宣称运行时验证覆盖。
专项真实模型8/8通过:两种不同延迟下的跨日换算、5次缺少历史基准的澄清,以及明确绝对时间更正。输出同时检查回复时间和表单时间,记录原始调用回复,不只检查状态码。补充14项首次13/14(明确无人伤未提取出字段),复跑14/14;原缺失历史时钟场景两轮都通过。没有删除或放宽原有断言。
离线测试覆盖首次提取失败、跨日、同轮双调用同一时钟、JSON序列化、LangGraph checkpoint、历史窗口截断、缺失/无效/无时区历史时钟、拒绝无法支持的日期或拍照、一次修复后仍失败不提交状态、安全中断优先、时间更正和旧会话具体候选。
最终全量单元测试171/171通过,退出码0(日志:`evals/results/time-boundary/unit-tests.log`;退出状态:`unit-result.json`)。原有46个cases首次45/46,唯一失败为 `browser_addon_non_motor_vehicle_zero_count_history` 的模型空响应;完整复跑46/46通过。首轮失败保留,没有通过修改case或断言掩盖。
46份原有YAML的SHA-256与上一轮优化前基线完全一致。工作流除采集与提取提示词外,其他节点配置与Git基线相同。最终机器可读核对结果:`evals/results/time-boundary/verification.json`;`git diff --check`通过。
## 范围
该修复作用于当前 LangGraph 文本采集节点。仅向外部FastGPT导入同步提示词,并不会获得本项目新增的服务器时钟记录与校验,不能据此宣称外部工作流同样完成修复。
持久性取决于项目使用的checkpointer。当前默认是内存checkpointer;本次保证其跨轮状态携带时间记录,没有新增进程重启后的数据库持久化能力。无法精确到分的“刚刚”等近似表达仍按提示词询问大概时间,不任意指定分钟。