# 第四步:会话流式接口(F026) ## 当前接口速览(2026-09-28,F118) 当前应用只注册三个路由:`POST /api/chat`、`POST /api/company/classify`、`GET /health`。前两者分别返回SSE事件流和普通JSON;`/health`只说明应用启动完成,不探测Redis、模型、Milvus或企业信息服务。以下按当前代码解释接口;下文按Feature保留的早期记录仅说明演进过程,若有差异以本节及[聊天接口](api-chat.md)、[分类接口](company-classification.md)为准。 `/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`/`memory_busy`,内存状态不可读取返回503 `memory_status_unavailable`。这些接纳错误均携带`Retry-After: 2`。成功接纳后的超时、处理失败及附件失败须监听SSE `error`,不能只看HTTP状态。运行时的并发和内存限额分别见[并发配置](concurrency.md)、[内存保护](memory-safety.md)。 F104:问答、分类与后台共享内存接纳,默认4个重任务名额;超内存预算返回429 memory_busy,采样不可用返回503 memory_status_unavailable,均带Retry-After。断线不提前归还在途任务名额。部署硬隔离需安装systemd unit,见[内存保护](memory-safety.md)。 F080(2026-09-21):区域前提与最终末尾补充按序进入同一正文流;正常纯追加路径end前补齐delta,真实校验/链接改写仍保留replace。GraphRuntime去除重复开头,独立answer提示放在summary结束后,避免插入未结束正文。卡片优先保持,需重启;前端按[流式协议](token-streaming.md)去重替换。 F079(2026-09-20):推荐完成筛选及卡片展示字段后,经请求作用域先发完整source/item,再生成综合正文增量;GraphRuntime节点完成时按来源ID去重,不重复发卡。result保持完整,CLI非流式行为保持;前端收到卡片即展示、下方接续正文。需重启后端,见[事件协议](token-streaming.md)。 F074(2026-09-20):chat新增可选pictures/files下载链接数组,省略/空数组兼容原调用。图片由独立视觉请求读取,Excel/DOCX/文字PDF由本地解析器读取;附件文字与知识联合综合并保留来源,长内容受分块及总预算控制。新增attachments状态事件及result字段,见[附件契约](chat-attachments.md)。不修改现有模型秘密配置;新增锁定解析依赖,重启后生效。原始下载URL不进入模型/检查点/日志;提取文字进入同会话检查点。公网下载、处理边界和确认恢复约定详见契约。 F111(2026-09-23):附件任一失败立即终止本轮,不向LangGraph提交部分附件;SSE错误直接标出图片/附件序号和原因,前端需展示`error.data.message`。详见[附件契约](chat-attachments.md)。 F073(2026-09-20):GraphRuntime建立请求作用域正文流,上游模型以真正的SSE读取公开正文片段,经原有界Job队列立即输出answer_start/delta,最终校验后answer_end整块替换,失败answer_abort撤回。只覆盖最终总结/知识正文和闲聊公开字段;阶段、来源、卡片与result保持。原answer/summary携带可选stream_id关联同一段,前端需监听新增事件,详见[正文流式协议](token-streaming.md)。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,具体见 [接口文档](api-chat.md)。 F035(2026-09-15 15:13):三项下一步建议合入现有summary事件的data.text末尾,无独立next_actions事件;result保留recommendation.next_actions供来源追溯。具体需求有推荐才追加,前端沿用总结字段无需修改。字段见 [接口文档](api-chat.md)。 面向调用方的详细返回结构、卡片字段、三类结果及错误处理见 [POST /api/chat接口文档](api-chat.md)。本页主要保留服务配置与实现说明。 Redis按请求保存历史,包含原始输入、接收/开始/结束时间和状态;日志与历史统一上海时间,见 [用户提问历史与时间](request-history.md)。F059已将生产入口改为DMS监视:前端仅保存问答草稿,后台负责助手会话创建和维护,API/CLI不再保存本地会话JSON。后台只读「助手问答记录」,按c_session_id聚合并判断静默,自动创建缺失「助手会话」草稿;已有摘要仅问答内容变化并再次静默后更新;需重启生效,详见 [DMS会话分析](session-analysis.md)。 代码在 `src/step4_api/`,复用第三步 LangGraph、Redis 检查点、模型与检索。启动: ```powershell .\.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`,便于对应分析。阶段、模型排队/调用、首卡首字和总耗时的口径见[耗时观测](response-timing.md)。该事件是后台日志,不是SSE事件;需重启后采集。 运行 `.\.venv\Scripts\python.exe -m src.step4_api` 后,业务日志默认以UTF-8单行JSON输出到终端stderr,等级为INFO;异常/降级使用ERROR/WARNING。通过应用工厂运行也在生命周期启动时配置日志,不重复添加输出器。不新增.env参数或文件日志,部署时可由服务管理器收集标准错误流。 合成示例(ID为占位值): ```json {"timestamp":"2026-09-14T17:00:00.000+08:00","level":"INFO","event":"request_received","message":"收到问答请求","request_id":"example-request","method":"POST","path":"/api/chat"} ``` ```json {"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"} ``` ```json {"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 | 问答、分类及后台共享的接纳名额与RSS预算;具体配置和拒绝语义见[内存保护](memory-safety.md) | | API_MEMORY_JOB_RESERVE_MB / API_MEMORY_MIN_AVAILABLE_MB | 256 / 512 | 每任务增长预留与系统可用内存底线,单位MiB | | 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;系统环境变量优先,非法值启动失败。修改后需重启。跨库原文读取从串行改为有界并发,同库首段与剩余分段仍按依赖顺序读取;全部分段、哈希校验、失败状态和候选顺序保持,不减少内容。见[并发约束与风险](concurrency.md)。 ## 请求与会话 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数组(见[附件契约](chat-attachments.md))。最小请求: ```json { "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: ```text 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响应文档](https://starlette.dev/responses/) 与 [Uvicorn设置](https://uvicorn.dev/settings/)。 ## 中间处理状态(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秒)发送: ```text 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`或`memory_busy`、503 `memory_status_unavailable`或`service_unavailable`。接纳拒绝的409/429/503附`Retry-After: 2`;422附件链接错误还可能带`message`和`attachment`。不要自动重发已执行中的业务请求。流开始后无法更改HTTP状态,必须处理`error`事件和没有`done`的异常断流。 ## 前端读取 原生 `EventSource` 不支持JSON POST,使用 `fetch`。以下示例适用于本服务的LF事件格式和同源前端;跨域可通过同源代理,当前未开放CORS。 ```javascript 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或高可用。 ## 验证 ```powershell .\.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记录去重前后字节数、重复项数量、最终与中间整理输出预留;最终和中间整理分别预算,原来源修复和超长兜底保持。配置与边界见[综合预算](synthesis-budget.md)。需重启后新请求生效。 `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日志;历史错误无法恢复具体原因。本次未修改长度阈值、重试或汇总规则。