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
+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;本次保证其跨轮状态携带时间记录,没有新增进程重启后的数据库持久化能力。无法精确到分的“刚刚”等近似表达仍按提示词询问大概时间,不任意指定分钟。