step4-api.md 31 KB

第四步:会话流式接口(F026)

当前接口速览(2026-09-28,F118)

当前应用只注册三个路由:POST /api/chat、POST /api/company/classify、GET /health。前两者分别返回SSE事件流和普通JSON;/health只说明应用启动完成,不探测Redis、模型、Milvus或企业信息服务。以下按当前代码解释接口;下文按Feature保留的早期记录仅说明演进过程,若有差异以本节及聊天接口、分类接口为准。

/api/chat仅接受JSON对象的thread_id、question及可选pictures、files,不接收额外字段。输入校验失败在流开始前返回JSON错误;接纳后为HTTP 200 SSE,事件统一包裹thread_id/request_id/data。当前正文可通过answer_start、answer_delta、answer_end、answer_abort增量到达;最终answer_end应按operation=replace覆盖草稿,result只作完整快照。政策卡片的source/item可先于讲解正文到达。附件处理另有attachments事件,处理失败直接以error结束。

同一thread_id并行提交会返回409 thread_busy;工作池或共享任务名额满返回429 server_busy。F151取消应用内存采样与预算拦截,正常接纳不再产生memory_busy/memory_status_unavailable;提交任务异常仍可503 service_unavailable。这些接纳错误均携带Retry-After: 2。成功接纳后的超时、处理失败及附件失败须监听SSE error,不能只看HTTP状态。运行时的并发和独立硬限制见并发配置、内存说明。

F104历史(内存采样拦截已由F151取消):问答、分类与后台共享接纳,默认4个重任务名额;当前只按名额接纳,保留断线不提前归还在途任务名额。部署硬隔离仍需安装systemd unit,见内存说明。

F080(2026-09-21):区域前提与最终末尾补充按序进入同一正文流;正常纯追加路径end前补齐delta,真实校验/链接改写仍保留replace。GraphRuntime去除重复开头,独立answer提示放在summary结束后,避免插入未结束正文。卡片优先保持,需重启;前端按流式协议去重替换。

F079(2026-09-20):推荐完成筛选及卡片展示字段后,经请求作用域先发完整source/item,再生成综合正文增量;GraphRuntime节点完成时按来源ID去重,不重复发卡。result保持完整,CLI非流式行为保持;前端收到卡片即展示、下方接续正文。需重启后端,见事件协议。

F074(2026-09-20):chat新增可选pictures/files下载链接数组,省略/空数组兼容原调用。图片由独立视觉请求读取,Excel/DOCX/文字PDF由本地解析器读取;附件文字与知识联合综合并保留来源,长内容受分块及总预算控制。新增attachments状态事件及result字段,见附件契约。不修改现有模型秘密配置;新增锁定解析依赖,重启后生效。原始下载URL不进入模型/检查点/日志;提取文字进入同会话检查点。公网下载、处理边界和确认恢复约定详见契约。

F111(2026-09-23):附件任一失败立即终止本轮,不向LangGraph提交部分附件;SSE错误直接标出图片/附件序号和原因,前端需展示error.data.message。详见附件契约。

F073(2026-09-20):GraphRuntime建立请求作用域正文流,上游模型以真正的SSE读取公开正文片段,经原有界Job队列立即输出answer_start/delta,最终校验后answer_end整块替换,失败answer_abort撤回。只覆盖最终总结/知识正文和闲聊公开字段;阶段、来源、卡片与result保持。原answer/summary携带可选stream_id关联同一段,前端需监听新增事件,详见正文流式协议。CLI/后台分析无请求流上下文时保持原完整调用。未增加依赖或模型轮次,外部代理与前端未部署验证。

F052最新:recommendation_status由LLM概括原文扶持标准,约10字。summary之外的短文案失败保存support_highlight_errors,并在result_ready.support_highlight_error_codes记录安全码、degraded=true;卡片仍返回,详细引用不写终端。

F049(2026-09-17):result_ready.candidate_outcome_counts 按固定处理结果统计数量, 例如 {"selected":1,"ineligible":6,"reference_only":2,"analysis_failed":1,"not_relevant":1}。 只记录固定枚举计数,未知值归入unknown,旧结果缺少诊断时为 {};不记录标题、来源ID、 企业事实或引文。详细 candidate_diagnostics 和扩充后的 excluded_items 由最终 result.data.recommendation 返回并随Redis检查点保存,见第三步F049说明。 无需前端新增展示;按request_id与session_ref定位新请求后读取诊断。

F049追加:result_ready.validation_error_counts 按安全错误码统计所有失败尝试, 包括重试成功前的失败,例如 {"invented_citation":1};因此最终analysis_error_count=0 时这里也可能非空。详细失败路径、引文与当批原文保存在候选的validation_attempts, 随Redis检查点和最终result返回,不进入终端日志或回答正文。

F045追加修正(2026-09-16 16:24):最新复发仍为model_invalid_content_json。现在仅对已知分析对象在EOF缺一个外层右花括号、且顶层/卡片字段集合完全符合预期的情况修复;若match_basis唯一错嵌card则移回顶层。所有文本原值不变,其他JSON损坏不修复,后续仍执行原引用/条件/卡片校验。16384输出、4000字节分块与最多一次格式重试保持。此规则替代此前完全不补括号的说明,不是宽松通用JSON修复。

F045修正(2026-09-16 16:18):输出上限保持16384。候选原文分块预算从15000字节降至4000字节,超长字段按1000字符无损切分,保留path/offset;最多12块的停止边界保持。明确顶层字段顺序,match_basis先于最后的card,避免嵌套错位和外层括号未闭合。无效JSON/非对象最多重试一次,与引用/结构修正共享两次总尝试;继续严格JSON解析和逐字引文校验,不自动补括号或改写原文。用户已授权原两条失败候选发送当前模型复验,审批阻塞已解除。

2026-09-16 16:11 更新(F044/F045):最新输出预算改为16384,作用于JsonModel共用的查询规划、候选分析和综合总结,上下文预留随常量同步;意图分类仍为2048。111项第三步、35项API及离线基线通过。无效JSON/非对象增加最多一次格式重试,与原引用校验共享两次总尝试,仍失败保留具体错误。未重启服务。F045真实候选复验因自动审批拒绝将Redis资料重发外部模型而待用户授权,尚不能宣称实际排水证问题修复。

F044(2026-09-16 16:06):F044:JsonModel输出上限4096→8192,作用于查询规划、候选分析和综合总结;上下文预留同步提高,不修改模型上下文配置或意图分类2048预算。候选分析错误及API日志区分HTTP状态、超时、网络、响应JSON/结构、内容JSON/结构、输出token截断和内容过滤;异常正文不输出。29项第一步、107项第三步、35项API回归及离线初始化通过。未更改自动重试,未调用真实模型或重启服务。

候选分析失败的 analysis_errors[].code 现细分为:

错误码 含义
model_http_400 / 401 / 429 / 5xx 上游HTTP状态码;实际保留具体三位数字
model_request_timeout 请求超时
model_network_failure 其他网络错误
model_invalid_response_json 上游HTTP响应不是合法JSON
model_invalid_response_schema 上游choices/message等结构异常
model_invalid_content_json 模型生成内容不是合法JSON
model_invalid_content_schema 模型生成JSON不是对象
model_output_token_limit finish_reason=length,输出达到token限制
model_content_filtered finish_reason=content_filter
model_incomplete_generation 其他非stop结束原因
model_response_too_large 上游响应超过读取上限
model_request_failed / model_unexpected_error 未列明的传输/内部异常,保留安全兜底

原引用校验错误码保留;历史model_service_failure不能回填细分原因。规划/总结既有降级错误包装保持。默认输出上限增加到8192,不代表强制生成更长回答,也不能解决网络或鉴权失败。共享transport将network_or_timeout拆为request_timeout/network_failure,第一步原有重试判定保持。

F042最新规则(2026-09-16):默认回答正文不附参考文献、资料依据、资料名称或原文摘录;原文、source_ids、citations及参考链接仍在结构化来源中保留。用户明确要求依据或原文时,按需提供相关短引文。此前自动展开来源的说明由本规则替代,SSE去重保持。

F042(2026-09-16 11:40):后端发送SSE前消除answer与summary的重复正文;总结包含概述时省略answer,仅保留独立备用结论或失败提示。资料依据补充原文引文,正文已为原答案时不再复制。result最终快照保持,详情见api-chat.md。需重启服务后生效。

F038:公司选择回传支持当前候选唯一全名、KeyNo、信用代码、候选JSON字符串及空白拼接的候选显示字段;question仍为字符串,保持同一thread_id。确认后继续原问题,详见api-chat.md第7节。

F037(2026-09-15 15:50):progress.py保留stage/status协议及真实节点时序,message改为自然服务表达;retrieve为“正在查阅与您咨询事项有关的政策和办事资料……”,recommend为“正在对照资料原文,把与您问题有关的重点说明白……”。completed只说明步骤结束,不保证查到结果;不引入原问题或个人数据。accepted/heartbeat文案同步更新;summary.text为Markdown,具体见 接口文档。

F035(2026-09-15 15:13):三项下一步建议合入现有summary事件的data.text末尾,无独立next_actions事件;result保留recommendation.next_actions供来源追溯。具体需求有推荐才追加,前端沿用总结字段无需修改。字段见 接口文档。

面向调用方的详细返回结构、卡片字段、三类结果及错误处理见 POST /api/chat接口文档。本页主要保留服务配置与实现说明。

Redis按请求保存历史,包含原始输入、接收/开始/结束时间和状态;日志与历史统一上海时间,见 用户提问历史与时间。F059已将生产入口改为DMS监视:前端仅保存问答草稿,后台负责助手会话创建和维护,API/CLI不再保存本地会话JSON。后台只读「助手问答记录」,按c_session_id聚合并判断静默,自动创建缺失「助手会话」草稿;已有摘要仅问答内容变化并再次静默后更新;需重启生效,详见 DMS会话分析。

代码在 src/step4_api/,复用第三步 LangGraph、Redis 检查点、模型与检索。启动:

.\.venv\Scripts\python.exe -m src.step4_api

先连接Redis、加载并预热本地Embedding,再监听。GET /health 返回 {"status":"ready"} 表示启动已完成,不代表QCC、Milvus、DeepSeek当前都健康。Uvicorn通用访问日志仍关闭;F031默认打印脱敏业务日志,详见下方“终端日志”。不打印请求正文、会话资料或环境配置。无前端页面。

终端日志(F031)

F097新增request_performance耗时汇总,按用户要求直接包含thread_id及request_id,便于对应分析。阶段、模型排队/调用、首卡首字和总耗时的口径见耗时观测。该事件是后台日志,不是SSE事件;需重启后采集。

运行 .\.venv\Scripts\python.exe -m src.step4_api 后,业务日志默认以UTF-8单行JSON输出到终端stderr,等级为INFO;异常/降级使用ERROR/WARNING。通过应用工厂运行也在生命周期启动时配置日志,不重复添加输出器。不新增.env参数或文件日志,部署时可由服务管理器收集标准错误流。

合成示例(ID为占位值):

{"timestamp":"2026-09-14T17:00:00.000+08:00","level":"INFO","event":"request_received","message":"收到问答请求","request_id":"example-request","method":"POST","path":"/api/chat"}
{"timestamp":"2026-09-14T17:00:00.200+08:00","level":"INFO","event":"stage","message":"正在检索相关政策和配套服务……","request_id":"example-request","session_ref":"0123456789abcdef","elapsed_ms":200,"stage":"retrieve","status":"running"}
{"timestamp":"2026-09-14T17:00:05.000+08:00","level":"INFO","event":"worker_finished","message":"后台处理结束,会话锁已释放","request_id":"example-request","session_ref":"0123456789abcdef","elapsed_ms":5000,"outcome":"completed"}
日志字段 中文含义
timestamp 上海时间,含毫秒与+08:00时区,不受服务器系统时区影响
level INFO正常信息、WARNING拒绝/降级、ERROR处理失败
event 下表的日志事件名,不是SSE事件协议
message 固定中文处理说明,不来自用户正文
request_id 服务端生成的本轮请求ID,与SSE及HTTP响应头X-Request-ID一致
session_ref thread_id的SHA-256前16位,仅供脱敏关联;不是原始thread_id、用户身份或鉴权凭证
elapsed_ms 毫秒耗时;任务日志从接纳起算,拒绝日志从进入请求处理函数起算
active_requests 接纳当时的活动问答数量
stage / status 实际处理阶段与状态,与进度事件对应
code / status_code 固定错误码 / HTTP拒绝状态码
degraded 结果是否检测到图错误、候选错误、完整性缺口或summary备用说明
error_count / analysis_error_count 图流程错误数 / 候选分析错误数
error_codes / analysis_error_codes 各自最多16个经过白名单筛选的错误码,未知内容写unknown_error
summary_status / summary_error_code / planning_error_code 综合说明状态、综合说明错误、查询规划错误
outcome 后台处理结果,见下文
reason / worker_finished 流结束原因 / 流关闭时后台线程是否已经结束
日志event 中文含义
service_starting / service_ready 服务正在初始化 / 已就绪
service_startup_failed 初始化失败
service_stopping / service_stopped 服务正在停止 / 已停止
request_received POST /api/chat进入处理函数;尚未完成参数校验
request_rejected 参数错误、同会话忙、容量满或无法提交任务
request_accepted 已通过校验与容量检查,进入任务调度
worker_started 后台开始处理
stage 实际节点开始/结束/等待输入/失败
result_ready 结果已生成,包含是否降级和错误数,不含正文
request_failed 工作流返回终止错误或发生未捕获异常
worker_finished 后台线程退出并释放会话锁,记录耗时与结果
stream_timeout HTTP流超时
stream_closed HTTP流结束,和后台处理结束分别记录

outcome为completed(后台完成)、needs_input(待用户输入)、degraded(生成了降级结果)、failed(处理失败)、stopped(被停止且未产生终止结果)、incomplete(未收到终止结果即返回)。completed不是政策资格已通过。

stream_closed.reason为terminal_event(已把done/error事件交给响应层)、client_disconnected(流被取消/提前关闭)、timeout(超过流时长)、worker_ended_without_terminal(工作线程结束但没有终止事件)。terminal_event不证明客户端已经收到全部字节。断线后后台同步调用可能仍运行,所以stream_closed可先于worker_finished,不会提前释放会话锁。

不记录完整问题、回答、政策原文、公司资料、原始会话ID、请求头、IP、密钥或原始异常堆栈;不逐条记录source/item内容,也不打印每次心跳。日志中的阶段文本从固定模板生成,模型返回的未知错误内容不透传。本文描述的是业务日志;Uvicorn/模型库仍可能输出自身启动信息。代理或Uvicorn在请求进入应用前拒绝的流量、GET /health不会产生这些问答请求日志。

配置

以下配置已追加到本地Git忽略的 .env;进程同名环境变量优先,修改后重启:

变量 默认值 含义
API_HOST 127.0.0.1 监听IPv4/IPv6;局域网访问可设为 0.0.0.0 或本机局域网IP
API_PORT 8000 端口,1–65535
API_MAX_CONCURRENT_REQUESTS 16 同时执行的问答数,1–128;超限立即429
API_MEMORY_MAX_JOBS / API_MEMORY_BUDGET_MB 4 / 4096 共享接纳名额仍生效;F151取消RSS预算拦截,旧预算仅兼容解析,见内存说明
API_MEMORY_JOB_RESERVE_MB / API_MEMORY_MIN_AVAILABLE_MB 256 / 512 F151后仅兼容解析与校验,不参与请求接纳,无需修改原配置
API_ATTACHMENT_BASE_URL 空 相对附件路径的下载地址前缀;空值只接受完整HTTP(S)链接
API_LLM_CONCURRENCY 8 进程内模型调用上限,1–128,覆盖意图、规划、逐项分析与综合说明
API_RETRIEVAL_CONCURRENCY 4 进程内同时检索的请求上限,1–16
KNOWLEDGE_ANALYSIS_CONCURRENCY 8 每请求全文分析共享线程池上限,1–32;候选协调池与卡片短文案阶段复用该配置,模型调用仍受API_LLM_CONCURRENCY约束
RETRIEVAL_CONTENT_CONCURRENCY 8 每请求跨库完整原文回读上限,1–32;当前六库实际最多6路,各库独立结果合并
API_HEARTBEAT_SECONDS 10 SSE空闲心跳间隔,1–60秒
API_STREAM_TIMEOUT_SECONDS 600 流最长持续时间,1–3600秒

仍复用 REDIS_URL、REDIS_CONTEXT_TTL_SECONDS 与第三步配置。检索内部每请求的查询/库并发仍由 RETRIEVAL_QUERY_CONCURRENCY 与 RETRIEVAL_BANK_CONCURRENCY 控制。数值需结合模型限流、GPU、Redis与Milvus容量调整,提高数值不保证更快。

F099(2026-09-22):API和CLI均读取新增两项配置,默认8,本地.env已显式设8;系统环境变量优先,非法值启动失败。修改后需重启。跨库原文读取从串行改为有界并发,同库首段与剩余分段仍按依赖顺序读取;全部分段、哈希校验、失败状态和候选顺序保持,不减少内容。见并发约束与风险。

请求与会话

2026-09-15最新三类约定(F034):仅闲聊smalltalk、政策了解policy_overview、具体需求specific_need。闲聊含身份、能力和使用方式介绍,由LLM依据现有角色信息自主回答,不检索或推荐政策;政策了解检索知识库,包含QA;具体需求按需画像、需求拆解、检索与适用性分析后依据原文回答。一般事实咨询归知识问答;无依据与服务失败分别说明,QA答案不因缺少卡片丢弃。 闲聊返回answer.text及空recommendation;政策了解/具体需求保留source、summary及knowledge_answers。事件名称和请求格式不变,需重启生效。

POST /api/chat,Content-Type: application/json,必填thread_id/question,可选pictures/files数组(见附件契约)。最小请求:

{
  "thread_id": "c81cf6a8-d628-4da0-bcef-f1930e775045",
  "question": "我想开一家小店,启动资金不够,有什么办法?"
}
  • thread_id:1–128个ASCII字母、数字、下划线或短横线。新窗口生成 crypto.randomUUID(),窗口内复用,切换窗口使用对应ID;未存在或Redis过期的ID开始新会话。
  • question:非空字符串,最多8000字符;保留原问题。请求体最多64KiB,读取超时15秒。
  • 收到 interrupt 后,相同ID的下一次 question 提交用户确认。company_need 输入公司名称/信用代码;不需要则输入“不需要”“无需公司信息”或 /skip。company_selection 输入候选显示序号(从1开始)确认,或输入新关键词;支持 /next、/retry、/cancel。也支持当前候选唯一全名、KeyNo、信用代码或候选JSON字符串;只按当前候选确定性匹配确认,不靠模型猜测。
  • 上一轮因故障或断线停在非补问节点时,输入 /retry 继续;其他输入返回 unfinished_turn,不会静默丢弃新问题。恢复可能重跑最后节点及外部调用,不保证恰好一次。
  • 已结束后再提新问题,沿用第三步规则重置本轮公司/画像。Redis持久化不等于历史指代理解。新窗口换ID,本接口不提供删除或历史列表。

SSE响应

HTTP 200 响应为 text/event-stream;事件以空行分隔,data 是JSON:

event: accepted
data: {"thread_id":"window-1","request_id":"...","data":{"message":"收到,我先了解一下您的问题。"}}

event: progress
data: {"thread_id":"window-1","request_id":"...","data":{"stage":"analyze","status":"running","message":"正在识别您的咨询需求……"}}

event: answer
data: {"thread_id":"window-1","request_id":"...","data":{"text":"回答内容"}}

event: done
data: {"thread_id":"window-1","request_id":"...","data":{"status":"completed"}}

事件 data内容
accepted 已接收;业务处理仍可能失败
progress 真实阶段的 stage、status 和可直接展示的中文 message
heartbeat 等待期间的 status=processing、中文 message、本次请求累计 elapsed_seconds
answer text:介绍或推荐概述
source 单个完整来源,含 full_record 等原契约
item 单项已校验推荐,含卡片、条件、匹配原因及引用
summary 综合说明对象
interrupt kind、问题、候选及 input_help
result response、attachments、cipa、company_info、完整 recommendation 和 errors
attachments 本轮图片/文件处理状态,含逐项结果;失败后仍有终止error
answer_start / answer_delta 建立正文块 / 按序追加临时正文片段
answer_end / answer_abort 以最终正文替换草稿 / 撤回失败草稿
done status=completed 或 needs_input,本次流结束
error 固定 code,发生后终止流,不返回原始异常

这是处理进度、正文增量和结果事件流。可公开正文经模型流式输出形成answer_delta;引用、来源链接及完整正文仍需最终校验,前端以answer_end替换草稿。source/item可在综合讲解前发送;result是完整最终快照,前端避免重复渲染。done不表示资格已满足或所有依赖成功,仍需查看errors及推荐的complete等字段。

F027将原空闲心跳注释改为可展示的 heartbeat 事件。服务设置 X-Accel-Buffering: no、Cache-Control: no-cache, no-transform;反向代理也需关闭缓冲并允许足够的读取超时。参考 Starlette响应文档 与 Uvicorn设置。

中间处理状态(F027)

节点真正开始执行前即发送 progress.status=running,包含中文提示。节点输出时发送 completed,表示该阶段处理结束,不表示政策资格已满足或外部服务一定成功;仍需检查最终错误字段。图节点抛错时发送 failed,随后接口返回终止错误;需要确认时发送 waiting_input,再返回 interrupt 和 done=needs_input。

stage 开始提示
session 正在读取会话信息……
prepare 正在整理您的问题……
analyze 正在识别您的咨询需求……
attachments 正在读取您提供的图片和附件……
direct 正在准备答复……
clarify / confirm 正在准备需要补充的信息 / 整理公司候选
search 正在查找相关公司信息……
basic / honors 正在查询工商信息 / 荣誉和资质信息
handoff 正在整理本轮需求与已有信息……
plan 正在拆解需求并规划检索方向……
retrieve 正在检索相关政策和配套服务……
recommend 正在分析政策原文、核对相关条件并整理回答……

只报告实际经过的阶段:了解服务不会出现政策检索;个人路径不默认出现公司查询。状态文本来自固定模板,图任务输入、原始异常、内部推理与未核验结果不透传。无需前端推测当前阶段,也不提供虚构完成百分比或预计剩余时间。

长时间没有新事件时,按 API_HEARTBEAT_SECONDS(默认10秒)发送:

event: heartbeat
data: {"thread_id":"window-1","request_id":"...","data":{"status":"processing","message":"还在为您准备答复,请再稍等一会儿。","elapsed_seconds":20}}

前端可在状态栏显示最近 progress.message,用 heartbeat.elapsed_seconds 更新等待时长。accepted 也增加 data.message 作为立即反馈。到达 interrupt 提示用户输入,到达 done/error 停止等待动画;不要把进度提示拼进最终回答正文。recommend 包括原文分析、条件比较和综合说明,当前未拆成逐篇政策的内部实时进度。

流开始前的错误为普通JSON {"error":"固定错误码"}:415类型错误、422参数非法、413过大、408请求体超时、409 thread_busy、429 server_busy、503 service_unavailable。F151取消内存接纳错误memory_busy/memory_status_unavailable,旧版本可能仍返回。接纳拒绝的409/429/503附Retry-After: 2;422附件链接错误还可能带message和attachment。不要自动重发已执行中的业务请求。流开始后无法更改HTTP状态,必须处理error事件和没有done的异常断流。

前端读取

原生 EventSource 不支持JSON POST,使用 fetch。以下示例适用于本服务的LF事件格式和同源前端;跨域可通过同源代理,当前未开放CORS。

async function ask(threadId, question, onEvent, signal) {
  const response = await fetch('/api/chat', {
    method: 'POST', headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({thread_id: threadId, question}), signal
  });
  if (!response.ok) throw new Error((await response.json()).error);
  const reader = response.body.getReader(), decoder = new TextDecoder();
  let pending = '', terminal = false;
  try {
    while (true) {
      const {value, done} = await reader.read();
      pending += done ? decoder.decode() : decoder.decode(value, {stream: true});
      let split;
      while ((split = pending.indexOf('\n\n')) !== -1) {
        const frame = pending.slice(0, split);
        pending = pending.slice(split + 2);
        if (frame.startsWith(':')) continue;
        const lines = frame.split('\n');
        const event = lines.find(line => line.startsWith('event: '))?.slice(7);
        const data = lines.filter(line => line.startsWith('data: ')).map(line => line.slice(6)).join('\n');
        if (!event || !data) continue;
        onEvent(event, JSON.parse(data));
        if (event === 'done' || event === 'error') terminal = true;
      }
      if (done) break;
    }
    if (!terminal) throw new Error('stream_disconnected');
  } finally {
    await reader.cancel();
    reader.releaseLock();
  }
}

并发及边界

异步ASGI承接请求,同步图在固定线程池执行,接纳前检查容量避免无限排队。相同ID执行中再次提交409,不同会话并行。检索仅对模型初始化、GPU推理及连接创建加锁,Milvus只读操作可跨请求重叠。模型调用、检索分别有进程级上限。

每请求最多缓冲8个事件,慢客户端会阻塞生产端继续写入。断线/超时停止后续事件与节点推进,但不能强制中断已发出的同步请求;槽位与会话锁保留到工作线程真正退出,避免旧请求与新请求同时写状态。底层服务超时仍生效。关闭服务先停止接纳、等待线程退出,再关闭连接。

本版支持单进程、多会话并发,入口固定 workers=1。不要增加worker或多个服务副本共享会话命名空间,也不要用CLI同时操作API会话:锁在进程内,尚无分布式排他机制。一个进程复用一份本地模型,避免重复显存占用。

尚无用户鉴权和会话归属校验。ID是窗口标识,不是权限;正式多用户前端需在服务/网关认证并校验归属。Redis历史未裁剪,长会话容量需另行评测;本次不承诺真实业务QPS或高可用。

验证

.\.venv\Scripts\python.exe -m unittest discover -s src/step4_api/tests -v
.\.venv\Scripts\python.exe -m unittest discover -s src/step3_question_answer/tests -q
.\.venv\Scripts\python.exe -m src.step4_api.smoke_check
# 真实预热并调用一次DeepSeek服务介绍:
.\.venv\Scripts\python.exe -m src.step4_api.smoke_check --real-runtime

默认smoke启动临时回环HTTP服务,真实Redis写随机合成会话,32路请求检查首事件、完成、持久化隔离和补问恢复;结束关闭服务并仅清理本次键。2026-09-14实测32/32完成,P95约0.553秒,最慢首事件0.062秒;分析节点是0.2秒延时替身,不能作为真实政策问答容量证据。另验证真实启动、预热和DeepSeek服务介绍,未压测完整QCC/Milvus/推荐链路。最终状态见feature_list。

F047 汇总失败诊断

F100新增综合输入无损去重及ANSWER_MAX_OUTPUT_TOKENS(默认/当前本地8192)。summary_diagnostics记录去重前后字节数、重复项数量、最终与中间整理输出预留;最终和中间整理分别预算,原来源修复和超长兜底保持。配置与边界见综合预算。需重启后新请求生效。

result_ready.summary_error_code保留具体安全错误码:analysis_context_limit为本地预算保护,model_request_timeout为超时,model_http_XXX为HTTP失败,model_invalid_content_json为模型JSON无效;summary_invalid_schema/summary_empty_text/summary_text_too_long/summary_invalid_sources分别为结构、空正文、长度和来源校验失败。未知异常仍为summary_generation_failed,不输出异常原文。

summary_diagnostics仅含固定阶段(model/validation)和数值:source_count、input_bytes、context_limit、output_token_reserve、margin_reserve、reserved_total、output_chars、text_limit。缺失项不输出;input_bytes是输入UTF-8字节上界,不是实际token计数。该诊断也保存在summary.diagnostics中。复测应在重启后记录同一request_id的result_ready日志;历史错误无法恢复具体原因。本次未修改长度阈值、重试或汇总规则。