fix(backends): make streamed replies and session state consistent

- Validate stage prefixes before forwarding model tokens. Invalid prefixes
  may use the existing repair budget; once output starts, validation failure
  aborts the turn without appending a regenerated reply. Bound the token
  queue and close inner streams when consumption stops.
- Replace delete-time lock eviction with reference-counted session locks
  covering holders and waiters. Serialize set/get with chat/stream/delete
  and release cancelled waiters without replacing a live lock.
- Store person synonyms under workflow-native driver keys and expose
  consistent alias views to single/group reads and graph prompt injection.
- Separate committed field patches from public formUpdate visibility.
  Commit callbacks keep repository fields synchronized even when updates
  are hidden or the consumer closes after graph completion.
- Disable OpenAI SDK automatic retries. Own bounded retry attempts in the
  application, use 1/2/4 second backoff, retry streams only before text is
  emitted, enforce attempt timeouts, and close upstream/client resources.
- Translate FastGPT SDK max_retries (total attempts) from the configured
  extra-retry count, and verify the real SDK loop uses four attempts.
- Document the per-LLM budget and streaming failure semantics without
  claiming the whole multi-call HTTP turn has the same timeout.

Validation: 267 offline tests passed, including actual SDK requests through
MockTransport, stream repair/abort, timeout/close, lock races, aliases and
formUpdate visibility; existing four dependency deprecation warnings.
This commit is contained in:
Eric Wang
2026-10-23 15:54:51 +08:00
parent 952bffe6f0
commit 24e2cfbe10
18 changed files with 852 additions and 316 deletions
+27 -7
View File
@@ -71,22 +71,42 @@
这修正了旧实现"workflow 真实人员数据无法分组读到"的问题;
- `driver{i}License` 非 state 键,对其 set_info 按未知键拒绝
(workflow 不消费它);
- 同义键(driver1ID vs sfzmhm1)暂不合并:合并需调用方确认
实际使用侧后作为后续优化,已列入审计项。
- 同义键统一存储:`sfzmhm{i}` 与 `driver{i}ID` 共用后者,
`sjhm{i}` 与 `driver{i}Phonenumber` 共用后者。单键和分组读取
继续提供两套键名;图输入同时注入一致的别名视图,避免旧 driver 值
遮盖 API 键的新值。同一批差量包含相互冲突的别名值时拒绝提交。
## 待审计项(作业 6-7 要求的后续工作)
- [ ] 审计现有调用方是否存在对 `set_info` 任意 key 写入的依赖
(如动态自定义键)。存在则在注册表补登记或提供逃生口。
**对账新增**:workflow 20261022 state 人员键
(`driver1ID`/`driver1License`/`driver1Phonenumber` ×2)
与 API 分组键不同名——确认线上调用方实际使用哪套键名,
必要时注册表补别名。
workflow 人员键 `driver{i}ID` / `driver{i}Phonenumber` 及对应
API 别名均已登记并统一存储;`driver{i}License` 只是车牌的
输出名称,外部写车牌继续使用 `hphm{i}`。
- [x] ~~流式 `chat` 的 `formUpdate` 差量写回 repository~~
(完整版已实现:FormUpdate 事件捕获后经映射写回,见 `app_service.stream`)。
(图提交时即同步业务字段;FormUpdate 是否返回不影响写回)。
- [ ] `get_info` 返回值中的敏感字段(证件号/手机号)回显边界:
注册表已记录 `SensitiveLevel`,访问控制随作业 9 落地。
## 流式、并发与字段一致性约定
- 阶段前缀先缓冲并按当前节点的转换规则校验,非法前缀不推送;
未输出时仍可使用一次修复预算。已开始输出后,全文格式或时间依据
校验失败会以错误终止,不在同一流中拼接修复答案,不提交本轮业务字段。
已经到达客户端的正文不可撤回;客户端只在收到 done 后将一轮视为成功。
- 会话锁引用包括持有者与等待者,只有最后一个使用者退出才回收。
chat、stream、set、get、delete 全部按同一会话串行;不同会话独立。
取消流时先关闭内部生成器、取消图任务,再释放会话锁。
- needFormUpdate 只决定对外是否返回差量。图提交时通过内部回调同步
repository,关闭差量显示或消费者在提交后断开不会丢失业务字段。
- SDK 自动重试关闭,由应用层统一控制;默认首次尝试加 3 次重试,
每次尝试总超时 60 秒,退避 1、2、4 秒。单次 LLM 调用理论预算
4×60+1+2+4=247 秒(另有响应关闭的清理时间)。FastGPT SDK 的
max_retries 实际代表总尝试数,适配层传入 4 以表达额外重试 3 次;
其 timeout 是 HTTP SDK 超时,不将它等同于整个多节点请求总期限。
流式首个文本 token 前可重试,发出文本后失败则终止,不重放。
该预算并非整个多次 LLM 调用的聊天回合总超时。
## 验证
- `test/services/test_fields_registry.py`:注册表完整性、类型/敏感级别、