# 用户提问历史与时间(F032) 2026-09-17 F059 DMS迁移:API/CLI已停止本地会话JSON保存,前端将逐轮问答保存到DMS草稿;后台只读问答,静默后分析更新「助手会话」。旧SESSION_DIR文件原样保留,不再观察。本页后续保留期限仅指Redis,上下文机制不变。详见 [DMS会话监视](session-analysis.md)。 2026-09-17 F057:请求历史列表与模型上下文是不同机制。此前请求虽已持久化,意图模型仍只读取本轮问题,导致“没有上下文”。现在从同会话图检查点恢复历史并供三类回答使用;旧检查点兼容恢复不修改旧回答。最近8轮/6000 UTF-8字节为模型历史窗口,省略会标记;不是将全部请求日志或历史政策答案作为事实依据。详情见step3-agent.md的Redis上下文持久化章节。 `POST /api/chat` 接纳的请求在Redis中额外保存一份普通JSON记录,供后续后台统计历史问题。每次HTTP请求一条记录,`request_id`与SSE、响应头`X-Request-ID`、终端日志一致。同一请求处理期间更新同一条记录,不会因多个LangGraph节点重复计数。原接口请求体和SSE结果结构不变。 日志及新增历史时间统一为上海时间(UTC+08:00),ISO 8601格式,包含毫秒和`+08:00`,不依赖服务器操作系统时区。已有日志不重写,旧会话检查点不回填无法确定的提问时间;LangGraph自身的检查点时间仍由框架维护。 ## 存储结构 - Redis类型:Hash,与原会话检查点使用同一键。 - 键:`business-assistant:context:v1:`。 - 历史字段名:JSON数组字符串 `['request','v1',request_id]` 的标准JSON编码,例如 `["request","v1","example-request"]`。 - 字段值:UTF-8 JSON;无需解析LangGraph的msgpack或Base64。检查点字段仍以`c`、`w`开头。 以下是合成示例,时间仅用于说明: ```json { "schema_version": 1, "request_id": "example-request", "thread_id": "example-window", "question": "我想了解创业方面的支持", "original_question": "我想了解创业方面的支持", "input_kind": "new_question", "received_at": "2026-09-14T17:00:00.000+08:00", "started_at": "2026-09-14T17:00:00.010+08:00", "finished_at": "2026-09-14T17:00:05.010+08:00", "duration_ms": 5000, "status": "completed", "degraded": false, "error_code": null } ``` | 字段 | 中文含义 | | --- | --- | | schema_version | 历史记录格式版本,目前为1 | | request_id | 单次请求标识;后台归档可作为唯一键 | | thread_id | 会话窗口标识,不是用户身份;一个用户可有多个窗口 | | question | 本次请求的原始输入,保留补问回复和命令 | | original_question | 本轮最初的问题;补问或故障恢复时仍保留最初问题,会话状态未读取成功时为null | | input_kind | 输入类别,详见下表 | | received_at | 请求进入接口处理函数的时间,早于请求体解析与接纳 | | started_at | 后台工作线程开始执行本次请求的时间 | | finished_at | 本次图处理结束时间;不等于用户收到全部网络数据,也不代表整段会话关闭 | | duration_ms | 从工作线程开始到图处理结束的毫秒耗时,使用单调时钟计算;包含等待模型/检索及事件队列,不含最终历史写入耗时 | | status | 处理状态,详见下表 | | degraded | 是否出现图错误、候选分析错误、结果不完整或备用综合说明;不代表资格是否符合 | | error_code | 失败固定码:processing_failed处理失败、unfinished_turn上一轮未完成;正常为null,不保存原始异常 | | input_kind取值 | 中文含义 | | --- | --- | | new_question | 开始新一轮问题;问题量统计通常筛选此项 | | clarification_reply | 回复当前公司补问/候选确认,含相关翻页或取消命令 | | retry | 无待补问但图未完成时,通过/retry恢复 | | unfinished_turn_input | 上轮故障未完成时输入了其他内容,被流程拒绝 | | unknown | 初始记录,尚未成功读取会话状态以判断输入类别 | | status取值 | 中文含义 | | --- | --- | | running | 已开始处理;finished_at/duration_ms为null | | completed | 本次处理完成;可能degraded=true,不能直接等同于高质量回答 | | needs_input | 本次处理结束,等待用户下一次补问回复;finished_at有值 | | failed | 本次处理失败,详见error_code | | stopped | 观察到断开连接、超时或服务关闭的停止信号,图未完成 | | incomplete | 图退出但未生成结束事件,且未观察到停止信号 | 请求验证失败、同会话忙409、超限429等未接纳请求不写提问历史,仍有脱敏日志。记录只由第四步API创建,CLI沿用原检查点机制。 先写running记录,再开始图执行;最终历史保存成功后才发送done。若Redis不可用,接口返回通用处理失败;首次写入失败可能没有记录,最终写入失败或进程被强制杀死可能留下running/null,后台不能把此状态自动当作成功。该记录是后台处理结果,具体断线/超时原因另按request_id关联流日志。 ## 后台读取 同项目Python代码可读取单个会话,按接收时间升序返回(同毫秒以request_id排序,不保证同毫秒先后): ```python from src.step3_question_answer.context import RedisContextSaver, load_context_config saver = RedisContextSaver(load_context_config()) try: records = saver.list_requests("example-window") new_questions = [r for r in records if r["input_kind"] == "new_question"] finally: saver.close() ``` 其他语言后台可直接用Redis客户端读取JSON。全量分析可使用`SCAN`遍历上述键前缀,再`HSCAN`遍历各Hash,仅解析字段数组前两项为`["request","v1"]`的值。使用游标分批读取;SCAN不是一致性快照,可能重复,归档必须按request_id幂等更新,尤其要更新最初读到running的记录。本轮没有新增历史查询HTTP接口、全局时间索引、用户账户字段或分析后台。 ## 保留时间 历史与检查点共享`REDIS_CONTEXT_TTL_SECONDS`:默认1296000秒(15天)。成功写入整会话续期,读取不续期;会话过期或调用`delete_thread`时历史一并删除。活跃会话的旧历史会随新写入继续保留,当前未做会话内历史裁剪。 这是有过期时间的会话存储,不是永久历史仓库。长期统计需要后台在过期前归档到持久数据库,不能依赖15天后仍能查到。问题原文仅写Redis业务历史,不写终端日志;后台访问权限需由后续服务控制,当前thread_id不能用于识别真实用户。