# POST /api/chat 接口文档 > 当前契约核对日期:2026-09-28(F118)。本页按Feature保留演进说明;实际路由与校验以`src/step4_api/app.py`、`runtime.py`为准。当前正文支持增量SSE,前端应使用下方的`answer_start/delta/end/abort`协议;较早章节中“结果全部生成后才发送”“不是逐token输出”等说法仅为当时实现,不再描述现行正文流。服务的三个路由总览见[第四步服务说明](step4-api.md)。 F114(2026-09-23):知识原文中的`图片URL`可按语义放在summary.text的对应正文位置,前端就地渲染该标记,不另建末尾图片列表。例如: ```text 这是该小程序的二维码: https://example.invalid/code.png 进入后按页面提示操作。 ``` 没有后续说明时可以直接以图片结束。本轮支持源资料已有image-scope标记:模型选择来源内的图片及位置,后端从已完成分析、最终采用的来源绑定HTTP(S)地址,相关图片的原文路径和标记保留在citations中;未选择的图片不自动追加。内部图片占位符不会发到answer_delta,图片URL在校验后的answer_end.text、summary.text和最终result中返回;前端沿用`operation=replace`覆盖同一回答块,避免继续显示“扫码”草稿或重复插入。无需新增图片事件。后端不下载/生成二维码,外部前端负责实际图片渲染;需重启后新请求生效。同题QA原文中的标记按既有原样回答契约保留。 F113(2026-09-23):summary.text中的办理链接改为“具体事项及情形(办事指南/网上办理入口)”,summary.detail_links.title同步采用原文情形名称。URL仍由后端按来源绑定,字段结构不变;前端按返回的Markdown链接文字展示,勿覆盖为统一“查看办事指南”。单项时限等短问题可直接回答并附一个相关入口,无需固定三栏。需重启后端,新请求生效,历史回答不回填。 ## 正文补充及提示顺序(F080,2026-09-21) 区域前提作为首段delta,最终校验后的末尾建议在end前补入delta;正常纯追加路径delta拼接与end一致。独立answer提示移到summary后,去掉重复的总结开头。end仍整块替换、summary按stream_id更新同一块、result仅校准;不要把每个text都直接追加。引用失败或链接绑定仍可能最终替换草稿。需重启后端,见[流式协议](token-streaming.md)。 ## 卡片先返回再讲解(F079,2026-09-20) 有政策时,完整 `source` / `item` 先返回,再输出综合讲解的 `answer_start` / `answer_delta`。前端收到item即可在正文上方展示卡片,不等result;节点结束不重复发卡片,result仅校准最终快照。首6张及展开规则不变,讲解降级时保留已校验卡片。后端需重启,详细顺序及边界见[正文流式协议](token-streaming.md)。 ## 图片与文件输入(F074,2026-09-20) F109(2026-09-23):`pictures`、`files` 可传相对路径,由后端按 `.env` 的 `API_ATTACHMENT_BASE_URL` 补为下载地址;当前本地前缀为 `http://121.43.55.7:10081/dms/`。完整HTTP(S)链接保持原样,空前缀不接收相对路径。详见[附件接口契约](chat-attachments.md)。 请求新增可选`pictures`与`files`链接数组,均可省略或为`[]`;例如`{"thread_id":"chat-1","question":"请结合附件分析","pictures":["https://files.example.com/image"],"files":["https://files.example.com/document"]}`。支持静态JPEG/PNG/GIF/WebP、Excel(XLSX/XLSM/XLS)、DOCX、文字PDF,按实际内容识别,无需URL扩展名。附件与知识证据联合回答,原question不改写。 新增`attachments`处理状态事件、`progress.stage=attachments`及`result.data.attachments`;完整提取内容通过原有source事件和`recommendation.sources`返回,类型`user_attachment`,不是政策卡片。失败和超限明确提示,超长文字按模型预算分块综合。公司确认/恢复时请省略附件或传空数组,原轮附件会保留。后续新问题不自动继承旧附件全文。格式范围、下载限制、token预算和前端字段见[附件接口契约](chat-attachments.md)。依赖安装后需重启后端,生产下载及外部前端尚待联调。 F111(2026-09-23):任一附件下载、格式或读取失败,SSE在`attachments`状态事件后直接返回`error.code=attachment_processing_failed`,`error.data.message`指出“图片N”或“附件N”及原因;不再做分类、检索或部分回答,也不返回`result`。前端应显示错误消息,并让用户替换附件后重新提交本轮问题。PDF任一页无可提取文字同样终止。 链接语法或协议在接纳前无效时仍为HTTP 422,JSON响应的`message`与`attachment`指出具体序号;前端需同时处理这一入口错误。 ## 正文增量流(F073,2026-09-20) 保留原阶段事件,新增 `answer_start` / `answer_delta` / `answer_end` / `answer_abort`。前端在 `answer_delta.data.text` 到达时追加正文,在 `answer_end` 用完整text替换;失败撤回临时正文。外层仍为thread_id/request_id/data。原answer/summary继续返回完整文本,可选stream_id用于关联同一正文并避免重复。仅监听旧事件仍会等完整答案;接入规则、样例、失败处理与验证见[正文流式协议](token-streaming.md)。后端需重启,外部前端需接入事件,result结构不变。 ## DMS优先公司检索(F069,2026-09-20) 先匹配DMS基础信息草稿;唯一全名/信用代码精确匹配直接读取基础资料,其他匹配展示最多3家供选择。基础资料确认并读取成功后才按信用代码查询荣誉,多条逐条返回,无记录为空列表。DMS无候选自动查询企查查,错误降级保留诊断。 DMS候选列表`interrupt.result.candidates`末尾自动追加“搜索更多公司”,最多3家公司加1个操作项,无需新增按钮。该项KeyNo为`action:search_more_companies`,其他公司字段为null。沿用原列表选择方式,保持thread_id,提交该项序号、名称、KeyNo或候选JSON字符串即可,也支持`/more`;`actions.more`保留兼容。操作项不会进入company_info或被当作公司查询。随后使用原线索查企查查,仍按精确匹配/候选确认规则处理;原有序号选择和企查查/next翻页保持。不要把action对象直接放进question。 `company_info`内部company/profile/honors结构保持,DMS业务字段转换为企查查命名并排除额外标签。DMS候选KeyNo使用`dms:1888:<记录ID>`;source.provider为DMS并提供栏目/记录ID及获取时间,document_url=null。前端不要假定所有KeyNo都是企查查ID或所有source都是企查查。详见[查询契约与字段映射](company-dms.md)。后端需重启,前端沿用原列表渲染和选择方式;历史结果不回填。 ## CIPA出海服务标记(F068,2026-09-20) SSE `result` 事件的 `data.cipa` 为布尔值,与 `response`、`company_info`、`recommendation`、`errors` 同级。用户询问CIPA、海外服务平台,或表达出海服务需求时为 `true`,否则为 `false`。复用需求识别LLM同次调用进行语义判断,不使用关键词匹配,不增加模型调用。 ```json {"response":"回答正文","cipa":true,"company_info":{},"recommendation":{},"errors":[]} ``` 各类回答及公司补问的result均提供该字段。结合必要历史理解指代,每个新问题重新判断;公司确认中断恢复保留原问题的判断。明确否定且未询问平台、国内业务和一般海外旅游/留学不触发。仅询问平台本身也触发,无需已提出办理需求。 模型缺失字段、返回字符串/数字等非布尔值或识别调用失败,走既有 `intent_analysis_failed` 降级,`cipa=false`;此时false不是成功判定不需要出海。旧检查点/手工模式无判断时默认false。前端读取 `result.data.cipa`,无需解析回答正文。需重启后端生效,历史不回填;该标记不代表服务办理、资格或政策匹配成功。 F067(2026-09-20):详情链接改为普通Markdown格式 [查看产品详情](https://example.invalid/detail?id=123),不再用尖括号包裹URL。地址中的括号等字符由程序编码,summary.detail_links.url与正文目标地址一致。某些前端会把尖括号目标识别成相对路径并拼上本站域名,本次从后端输出消除此兼容问题;原数据地址、来源绑定与SSE字段不变。需重启后端重新提问,已保存旧回答不会自动修复。 F066(2026-09-20):办事指南/办理事项介绍默认固定为一句话前置摘要(无标题),随后依次“### 办理渠道”“### 申请材料”“### 办理流程”。渠道说明原文支持的入口/窗口及限制,材料用列表并保留份数、原件/复印件和条件性要求,流程按原文步骤编号。缺失栏目保留并明确资料未说明,不补造渠道、材料、时限或通用审批步骤;跨事项/地区分别标明。其他相关知识可在三栏之后补充。用户明确只问某项细节时可聚焦该项,同题QA保留原文展示契约。 service_guide短答案也经过综合生成,详细排版规则集中在最终答复提示,分析阶段仅保留简短顺序要求;分层笔记保留渠道/材料/流程对应关系。summary.text与来源字段契约不变,前端按Markdown渲染,无新增接口。合成检查通过不保证所有真实模型输出始终遵循结构;需重启后端后新请求生效,历史不回填。 F065(2026-09-20):金融产品正文按“产品名称(机构)+默认2至3个关键要点”介绍,优先金额覆盖、额度/期限、融资成本与关键条件;保留“最高/起/年化”、担保及资格门槛,不逐字段铺陈。明确追问详情可展开,无字数硬截断。金融库短答案也走综合生成;同题QA仍保留原文路径,政策与其他知识联合正文保持。 每个采用且有有效地址的金融产品,在介绍旁显示Markdown“查看产品详情”链接。地址优先使用完整记录data.detail_url,缺失/非法时尝试source.url,仅接受无凭据HTTP(S)地址,不推测详情接口。模型只返回产品标记,程序绑定实际引用的source_id并插入真实存储地址;漏标记优先补到对应标题段末,无法定位则在正文附产品名和直达链接。缺地址时不制造链接,完整参考资料仍保留。 summary新增可选detail_links数组,每项source_id/title/url;knowledge_answers补充knowledge_type/detail_url。summary.text已含Markdown链接,前端正常渲染并允许这些已绑定URL即可点击,不应再追加一份知识正文或详情链接列表。source仍返回完整原文。CLI HTML同步仅启用绑定的产品链接,其余正文链接不激活;未新增产品卡片或改动SSE事件。外部前端需联调其Markdown链接支持;后端重启后新请求生效,历史不回填。 F056(2026-09-17):`items`仍返回全部入选政策,前端初始展示按数组顺序取前6项。首6项优先覆盖不同支持类别,同类别优先惠企来源,再按问题关联与主体依据;其余政策后移而不删除。新增`items[].policy_category`(主要支持类别)及`relevance_level`(direct/adjacent/unspecified)。F064更新:正文的政策范围仍为最终`items[:6]`;同时存在相关非卡片知识时,共同生成`summary.text`,实际采用来源关联`source_ids`与`citations`,完整原文保留在`sources`参考资料。地区前提、政策补问和下一步仍依据首六政策。联合整理失败明确说明失败并保留参考资料。无卡片及同题QA仍走原知识问答。`presentation.summary_policy_limit=6`;展开更多卡片不会自动重写已有总结。`retrieval.per_type_limit=3`表示每条查询每库取3条,`candidate_limit=12`表示多查询六库合并后最多12候选。新请求需重启后端加载,历史结果不回填。 F046(2026-09-16):新代码仅允许四个来源文件(人才政策和服务、涉外政策、惠企政策、上海虹桥国际中央商务区管理委员会政策和申报指南)的原条目 `raw_record.is_policy` 严格为JSON布尔 `true` 时进入政策卡片;字段缺失、false、字符串或数字均不符合。`card.kind` 仅为 `policy`。其他知识仍可贡献正文、引用与完整来源,不进入items。来源库类型及 `is_policy_library` 仍表示实际分库,不等同政策标记;人才/涉外/虹桥政策可能来自article库。过期、不符合条件及引用校验仍适用。须完成新知识摘要/入库并重启服务后整体生效,历史结果不回填。 F043(2026-09-16 15:56):公司检索后,当前用户公司线索与唯一候选的Name或CreditCode完整精确相等时,直接获取工商/荣誉并继续回答,不发送company_selection中断。仅去除线索首尾空白,不忽略内部字符、括号、后缀或大小写;简称、多个完全匹配、无结果及失败保持确认/补问。补充全名后再次搜索也适用;工商返回仍核对KeyNo/信用代码。旧等待确认的检查点仍沿原动作恢复,新问题清空旧画像。 F042最新规则(2026-09-16):默认回答正文不附参考文献、资料依据、资料名称或原文摘录;原文、source_ids、citations及参考链接仍在结构化来源中保留。用户明确要求依据或原文时,按需提供相关短引文。此前自动展开来源的说明由本规则替代,SSE去重保持。 ## 公司未找到时的返回(F041,2026-09-16) 10:18补修:企查查886实际以业务状态码201及空Result表示无结果,适配器现正确转换为 `not_found`,不再错误返回 `search:provider_failure` 和重试提示。其他服务错误仍单独提示失败。旧会话已保存的错误不会自动改写;新查询使用新规则,运行中的服务需重启加载代码。 公司搜索无候选时返回 `interrupt.data.kind=company_need`,表示需要补充信息;仅有候选可选时返回 `company_selection`。`interrupt.data.status`:`not_found`为正常查询无候选,`failed`为服务失败(同样使用company_need),`found`为有候选待确认。空结果不等于公司不存在。 初次询问是否需要公司信息的company_need没有result;搜索后的company_need带有result/status/actions。两者通过相同thread_id和question继续,服务端按是否存在搜索result选择正确恢复协议。无需前端构造内部action;旧检查点的company_selection仍可补充名称或跳过,下一次生成中断时使用新kind。 `not_found` 时 `interrupt.data.question`、`answer.data.text` 和 `result.data.response` 同步返回: > 本次未检索到与您提供的名称或代码匹配的企业。请核对并补充工商注册全名或统一社会信用代码;也可以输入“跳过公司查询”,先按原问题了解相关政策。 此时 `company_info={}`、`done.status=needs_input`,`actions`为 `refine/skip/cancel`。前端按question或answer展示一次提示,不重复追加response。保持相同thread_id,通过question发送新名称/代码继续查询,或发送“跳过公司查询”、`/skip`、“不需要”、“无需公司信息”跳过画像并按原问题继续;不代表企业身份或资格得到确认。 `failed`时明确说明查询服务暂时未能完成、无法判断是否有匹配企业,保留错误码并提供 `/retry`。两种情况均暂停等待用户,不自动重查;同一轮至多查询5次,下次查询尝试触发 `search_limit` 并退出公司确认。既有 `/cancel` 保留。 ## 公司信息(F040,2026-09-16) 此字段可直接作为 `POST /api/company/classify` 请求体,返回六角度多标签分类;无需外层包装。见 [公司分类接口](company-classification.md)。 SSE `result` 事件的 `data` 新增字典 `company_info`(2026-09-18纠正原拼写compang_info),与 `response`、`recommendation`、`errors` 同级。用户确认公司、工商查询成功后返回以下结构: ```json { "company_info": { "company": {"KeyNo": "synthetic-key", "Name": "合成公司"}, "profile": { "data": {"KeyNo": "synthetic-key", "Name": "合成公司"}, "source": {"provider": "企查查", "api_code": "410", "fetched_at": "2026-09-16T01:00:00+00:00"} }, "honors": { "records": [], "sources": [{"provider": "企查查", "api_code": "1001", "fetched_at": "2026-09-16T01:00:01+00:00"}], "complete": true, "error": null } } } ``` 这是合成字段片段;`profile.data` 保留工商接口的全部原始字段,`profile.source` 保留来源与获取时间。`honors.records` 每项为 `{data, source}`,保留各条荣誉原始字段及对应页来源;`honors.sources` 为查询页来源。成功但未查到荣誉时 `records=[]`、`complete=true`;查询失败或页数上限保留已查到的记录,`complete=false`、`error` 标明原因。工商已成功但荣誉尚未执行时 `honors=null`。 未查询、候选尚未确认、取消或工商查询失败时 `company_info={}`;失败仍通过既有 `errors` 报告,不能把失败当作没有资料。旧会话没有新增字段时也返回 `{}`,不回填历史结果。 工商和荣誉节点分别将此字典写入图状态,由现有 `RedisContextSaver` 保存到当前会话检查点,默认随会话写入续期15天(`REDIS_CONTEXT_TTL_SECONDS`)。读取方式为 `graph.get_state(config).values['compang_info']`,其中 `config.configurable.thread_id` 为当前窗口ID。无需新增Redis配置或独立全局公司缓存。新问题先重置当前公司字段,F057仅在明确延续同一已确认企业时恢复上一轮画像;旧检查点仍随会话保留到过期。仅原有 `result` 事件新增字段,无独立公司信息事件。 ## 回应式开场与Markdown总结(F037,2026-09-15 15:50) `summary.data.text` 和最终 `recommendation.summary.text` 为Markdown正文,保留回应问题的首段结论,按需使用 `###` 小标题、`**重点**`、无序列表及末尾三项编号动作。F042不再额外发送与总结重叠的answer开场,独立备用结论或提示除外。不使用外层代码围栏;简单无依据答复可以只有一段,不强行凑栏目。同题QA原答案保持引用块,先答案后资料名称。 summary末尾示意(格式示例,非真实政策): ```markdown 这项支持可以帮助您了解开店准备和经营管理。 ### 可以先了解的支持 - **创业培训**:具体内容以对应原文为依据。 ### 下一步建议 1. **确认相关地区**:具体问题由已检索到的条款生成。 说明这项信息为什么影响申请判断。 ``` 实际具体需求仍输出三项动作;上例只展示一项的排版。结构化next_actions与来源仍保留,不新增事件。前端继续读取summary.text,使用Markdown渲染并禁用/清理不可信HTML,不要再显示独立动作块。CLI输出Markdown文本,本地HTML支持标题/列表/加粗/引用,禁用Markdown内嵌HTML、图片与链接,原始来源仍由专门的安全链接展示。 `progress.data.stage/status/message`字段与时序不变,只更新用户文案:查阅资料、对照原文、了解情况等;accepted为“收到,我先了解一下您的问题。”,heartbeat为“还在为您准备答复,请再稍等一会儿。”。不把用户原话插入提示,不声称已找到政策、已满足资格或已经办理。 ## 惠企政策库标识与排序(F036,2026-09-15 15:25) 每个 `item` 新增 `knowledge_type`(实际来源库类型)和 `is_policy_library`(是否来自惠企政策库,布尔值)。例如 `{"knowledge_type":"policy","is_policy_library":true}`。这两个字段来自检索候选的type,不依赖模型生成的card.kind或名称;其他库中的政策卡片也会返回false。政策正式名称保持不变,前端可根据true显示“惠企政策库”标识;CLI/HTML已显示该标识。 推荐 `items` 及SSE的item事件按以下顺序返回,`priority.rank`连续编号: 1. 所有通过既有筛选规则的惠企政策库推荐。 2. 其他知识库推荐。 F056当前先按惠企来源、direct/adjacent/unspecified问题关联、主体证据排序,再用每个支持类别的最高优先项组成前六卡,不足六类按基础顺序补足,剩余政策保留。同类政策仍惠企优先;为覆盖不同类别,其他来源的新类别可排在惠企重复类别前。政策了解不比较用户资格。`presentation.sorted_by=category_diversity_then_policy_library_relevance_evidence`。同名去重优先保留合格的惠企政策库版本,其他全文来源保留。 来源优先级不绕过过期、不适用、主体冲突、缺少个人匹配依据等筛选规则;标识代表来源库,不代表已获批或已实时核验效力。六库召回范围和候选预算不变,排序只针对本轮已召回并入选的推荐。summary末尾三项动作规则不变,其选取随新的推荐顺序执行。 ## 下一步建议(F035,2026-09-15 15:13) 具体需求有入选卡片时,三项下一步建议以“下一步建议:”及1/2/3编号追加到现有 `summary.text` 末尾,保留原综合结论。前端沿用现有总结字段即可展示,无需新增区域或展开政策详情。顺序仍为 `items → summary`。政策了解、纯QA和无推荐不追加。 SSE现有 `summary` 事件的 `data.text` 和最终 `result.data.recommendation.summary.text` 均包含下一步建议,不再发送独立 `next_actions` 事件。`recommendation.next_actions`仍保留结构化数据与引文供追溯,前端无需消费或重复渲染。summary.source_ids包含原结论与动作的来源并去重;动作编号仅在本轮内有效。 每项字段:`id`(本轮编号)、`kind`(动作类型)、`title`(标题)、`description`(具体动作或待回答的问题)、`reason`(原因)、`source_ids`(关联政策来源)、`citations`(`source_id/path/quote`,逐字原文依据)。`source_id` 对应已有 `sources[].id`,可据此打开详情。 优先按推荐顺序提取有原文引文的待确认条件,同维度且条件说明去除空白后相同则合并来源,最多三项;剩余位置提供比较支持内容、查看申报条件、选择事项或整理疑问等阅读动作。不是语义同义去重,也不表示已核实模型条件解释。复用现有分析,不增加模型请求;原有 `follow_up_questions` 保留兼容,页面不必同时重复展示。 这是建议展示数据,不是自动执行指令,也不触发申请或补问中断。F057已支持同thread追问承接,无需每轮重复全部原问题;“是/否”仍需根据前文确定所指条件,有歧义时澄清,不能直接视为已完成资格更新。继续咨询会重新检索分析,不是同一候选增量重算。仓库不包含外部前端,本次仅完成后端、CLI和本地HTML。 **看不懂英文key或value时,先查 [字段与取值中文对照表](api-chat-fields-zh.md)**:逐层说明字段中文名称、状态/类型/原因的含义,以及前端该展示哪些内容。 服务名称:青浦区营商助手。本页最初建立于F026–F028,已随当前接口更新至F118;下方示例以合成数据说明字段,具体时序以本页当前事件总览和[正文流式协议](token-streaming.md)为准。启动与部署配置见[第四步服务说明](step4-api.md)。 **一次请求返回一串SSE事件,不是一个普通JSON响应。** 所有事件使用相同外层结构;普通聊天返回文字,政策与具体需求还会返回来源、卡片、综合说明。本文政策和企业名称均为合成示例,不代表存在对应政策或用户具备资格。标注“片段”的示例仅展示部分字段。 ## 1. 接口与请求 | 项目 | 说明 | | --- | --- | | 方法 / 路径 | `POST /api/chat` | | 地址 | `http://:/api/chat`;客户端使用实际服务IP/域名,不使用监听地址0.0.0.0 | | 请求类型 | `Content-Type: application/json` | | 建议响应类型 | `Accept: text/event-stream`(当前不强制校验) | | 成功响应 | HTTP 200,`Content-Type: text/event-stream; charset=utf-8` | | 请求追踪 | 应用处理的成功/拒绝响应增加 `X-Request-ID`,与服务端日志及成功SSE的request_id一致 | | 默认配置 | 本机127.0.0.1:8000,监听地址和端口由环境配置控制 | ```json { "thread_id": "window-20260914-001", "question": "我想开一家小店,但启动资金不够,有什么办法?" } ``` | 参数 | 类型 | 必填 | 约束与含义 | | --- | --- | --- | --- | | thread_id | string | 是 | 窗口标识,1–128字符,仅ASCII字母、数字、`_`、`-` | | question | string | 是 | 本轮输入,不能为空或全空白,最多8000字符 | | pictures | string[] | 否 | 图片下载链接数组,默认[];与files合计最多8个 | | files | string[] | 否 | Excel/DOCX/文字PDF下载链接数组,默认[] | 只接受上表四个字段,多余字段也返回422。请求体最多65536字节,读取请求体超时15秒。新窗口可生成 `crypto.randomUUID()`;同一窗口复用ID。未存在或Redis已过期的ID开始新会话。当前ID不是用户鉴权凭证。附件细节见[附件契约](chat-attachments.md)。 ## 2. 返回的两层结构 网络上的一条事件如下,**最后一个空行是事件结束标记**: ```text event: progress data: {"thread_id":"window-20260914-001","request_id":"a78b17a3b38241fa83c8ee91f40ec608","data":{"stage":"retrieve","status":"running","message":"正在检索相关政策和配套服务……"}} ``` `event:` 指定事件名称;`data:` 后面才是可用 `JSON.parse()` 解析的JSON: ```json { "thread_id": "window-20260914-001", "request_id": "a78b17a3b38241fa83c8ee91f40ec608", "data": { "stage": "retrieve", "status": "running", "message": "正在检索相关政策和配套服务……" } } ``` | 外层字段 | 类型 | 说明 | | --- | --- | --- | | thread_id | string | 本轮所属窗口,等于请求值 | | request_id | string | 服务端为本次HTTP请求生成的32位十六进制ID;同一流内保持一致,下一次请求会改变 | | data | object | 事件业务内容,由event名称决定 | 事件名称不在JSON对象中,没有 `type`、`event` 或 `code=200` 这类统一业务字段。除完整网络示例外,下文各事件的JSON示例都只展示这个内层 **data对象**。 一个网络数据块可能包含半条事件或多条事件,中文UTF-8字符也可能跨块;不可对每次 `reader.read()` 的结果直接执行 `JSON.parse()`。 ## 3. 事件总览与顺序 | event | 主要data字段 | 前端用途 | | --- | --- | --- | | accepted | message | 立即显示“已收到问题” | | progress | stage、status、message | 更新当前处理阶段 | | heartbeat | status、message、elapsed_seconds | 等待期间更新已耗时 | | answer_start / answer_delta | stream_id、target、sequence及增量text | 创建正文块并追加临时内容 | | answer_end / answer_abort | operation及最终text或撤回原因 | 替换最终正文或丢弃临时正文 | | attachments | items | 返回本轮附件逐项处理状态 | | answer | text | 普通对话正文、补问或不与总结重复的独立提示;推荐时可能省略 | | source | id、title、full_record等 | 保存完整来源,供引用详情查看 | | item | source_id、card、answer、conditions等 | 渲染单张政策/服务推荐卡片 | | summary | text、source_ids、status、notices | 显示末尾综合说明 | | interrupt | kind、question、input_help等 | 提示用户补充公司信息或确认候选 | | result | response、attachments、cipa、recommendation、company_info、errors | 当前轮次的完整最终快照 | | done | status | 本次流正常结束或等待输入 | | error | code,可选message | 本次流异常结束 | 正常顺序(`…`仅表示零个或多个事件,不是协议字符): ```text 普通对话:accepted → progress… → [answer_start → answer_delta… → answer_end] → answer → result → done 政策/需求:accepted → progress… → source… → item… → [answer_start → answer_delta… → answer_end] → summary → result → done 公司补问:accepted → progress… → interrupt → result → done(needs_input) 流中失败:accepted → 已产生的事件… → error ``` F042:后端省略与summary相同或已包含在其中的answer正文,前端无需判断二者是否重复。缺少总结、独立备用结论或部分失败提示仍可在source前发送answer。summary为空时不发送summary事件。result仍是完整快照,不作为新的正文追加。 耗时期间可穿插heartbeat,失败可能发生在任一阶段。不保证有source/item;不保证每次请求都经过所有progress阶段。`error`之后不保证有result或done。 这是阶段、正文增量与最终结果的事件流。临时`answer_delta`在模型公开正文生成时到达,最终引用、链接与图片绑定可能修正正文;始终以`answer_end.text`替换临时块。`source/item`在推荐卡片完成核验后可先于综合正文发送,`summary/result`用于最终校准,不能重复追加。完整规则见[正文流式协议](token-streaming.md)。 ## 4. 处理中间状态 ### accepted ```json {"message":"已收到您的问题,正在处理……"} ``` 表示已接纳请求,不表示模型调用或业务处理成功。 ### progress ```json {"stage":"retrieve","status":"running","message":"正在检索相关政策和配套服务……"} ``` | status | 含义 | | --- | --- | | running | 阶段开始,先通知再执行耗时节点 | | completed | 该阶段执行结束;不等于政策符合或所有依赖成功 | | waiting_input | 需要用户输入,随后有interrupt | | failed | 节点抛出异常,随后终止错误;已被业务捕获的失败仍可能表现为completed并在结果中说明 | | stage | 阶段 | | --- | --- | | session | 读取会话 | | prepare | 整理问题 | | analyze | LLM需求识别;普通对话回复也在这次调用生成 | | direct | 输出普通对话答复 | | clarify | 准备公司信息补问 | | search | 查询公司候选 | | confirm | 整理公司候选供用户确认 | | basic | 获取已确认公司的工商信息 | | honors | 查询荣誉与资质 | | handoff | 整理需求与已有信息 | | plan | 规划政策检索方向 | | retrieve | 检索相关政策与服务 | | recommend | 原文分析、条件比较和回答整理 | 前端优先直接展示message。相同阶段可能因恢复或重新查询再次出现,不能将stage当作全会话唯一事件ID。不提供百分比或预计剩余时间。 ### heartbeat ```json {"status":"processing","message":"问题仍在处理中,请稍候……","elapsed_seconds":20} ``` 无可发送结果且任务仍执行时按配置间隔发送,默认约10秒;不是严格定时器。elapsed_seconds是本次请求累计耗时的整数秒,不是当前节点耗时,也不是剩余时间。收到done/error后停止等待动画。 ## 5. 普通对话:文字结果 F034仅三类:闲聊smalltalk、政策了解policy_overview、具体需求specific_need。闲聊含问候、身份、能力介绍,由LLM依据现有角色信息自主回答;例如“你能干嘛”。政策了解检索六库含QA;具体需求按需画像、拆解、检索和适用性分析。 `answer.data`: ```json {"text":"辛苦了,先歇一歇,喝点水,给自己留一点放松的时间。"} ``` `result.data`: ```json { "response": "辛苦了,先歇一歇,喝点水,给自己留一点放松的时间。", "recommendation": {}, "company_info": {}, "errors": [] } ``` `done.data`: ```json {"status":"completed"} ``` answer.text和result.response是同一回复,后者不要再追加一次。纯闲聊没有item/source/summary,其内部smalltalk分类不单独暴露。其他咨询按知识库结果处理,可能没有item但有answer/source/summary,不能因卡片为空隐藏答复。 ### QA与无卡片知识答复(F033) 非闲聊统一走检索。recommendation新增knowledge_answers列表,每项包含source_id、title、answer、citations(path/quote)与exact_qa。同题QA保留原答案,summary.status=exact_qa;同题答案冲突为qa_conflict并并列原文;非同题有引用答复为knowledge_answers。summary新增可选citations,每项多带source_id,关联source.full_record的具体字段。 没有卡片时知识答复通过summary.text发送;后端不再同时发送相同answer。overview和result.response仍保留完整快照字段。资料依据明确标注资料名称与逐字原文;正文已经是原答案时说明其原文身份,不再复制一遍。sources提供完整原记录,原始链接缺失也不丢弃答复。不提供无依据的常识补答;无依据为no_evidence,检索/分析失败为unavailable。summary.notices及complete继续反映部分失败。旧no_recommendations仍是底层卡片摘要状态,不能据此认定没有知识答案。 模型调用或生成格式失败时,可能仍收到固定兜底文字、result及done=completed。应检查result.errors,例如 `intent_analysis_failed` 或 `direct_response_unavailable`,不能只凭done判断内容生成成功。 ## 6. 政策与具体需求:结构化结果 两类共用推荐数据结构,区别如下: | 内容 | 了解政策 | 具体需求 | | --- | --- | --- | | 示例 | “有哪些创业扶持政策?” | “我想开店但资金不足” | | recommendation.conversation_kind | policy_overview | specific_need | | audience | general | individual或enterprise | | 卡片顺序 | 保留原检索顺序 | 按需求与主体依据优先级排序 | | eligibility | not_evaluated | 当前入选条目通常为待确认 | | conditions | 通常为空,不做用户资格判断 | 地区、主体、行业、时间等条件核验 | | follow_up_questions | 空数组 | 需要确认的关键条件,最多5条 | | 公司确认 | 不需要 | 仅在当前需求需要企业事实时触发 | **卡片正文在item.answer及item.card,完整结论在summary.text。** 与总结重叠的answer由后端省略,不能只显示answer。 ### 6.1 result.data示意结构 以下为“成功检索但没有相关依据”的合成示例;有推荐时items/sources用后续小节的对象填充: ```json { "response": "当前知识库未找到足够相关的原文依据,不能据此认定不存在相关政策。请补充具体需求。", "attachments": [], "cipa": false, "company_info": {}, "recommendation": { "overview": "当前知识库未找到足够相关的原文依据,不能据此认定不存在相关政策。请补充具体需求。", "conversation_kind": "policy_overview", "audience": "general", "items": [], "sources": [], "follow_up_questions": [], "summary": { "text": "本轮没有筛选出可展示的具体推荐。可补充希望解决的事项,例如融资、设备更新或人才招聘,以便进一步查找。", "source_ids": [], "status": "no_recommendations", "notices": [] }, "excluded_items": [], "presentation": {"order":["items","summary"],"sorted_by":"subject_policy_evidence"}, "retrieval_analysis_complete": true, "complete": true, "company_context": {"profile_verified":false,"honors_complete":null,"errors":[]}, "analysis_errors": [], "retrieval": { "queries": ["合成主题查询"], "failed_databases": [], "failed_queries": [], "content_failures": [], "complete": true, "snapshot": "0000000000000000000000000000000000000000000000000000000000000000", "searched_types": ["policy","finance","qa","article","service_guide","service_catalog"], "candidate_limit": 12 }, "search_plan": {"needs":[],"queries":[],"error":null} }, "errors": [] } ``` 这里的snapshot为占位值;searched_types实际为当前检索类型列表,不应据本例写死。retrieval为后端诊断数据,降级时部分键可能不存在。示例中的推荐子字段说明形状,不保证每一种路径都返回相同子字段。 | 字段(相对recommendation) | 类型 | 说明 | | --- | --- | --- | | overview | string | 与result.response一致的概述;流式answer可能省略重叠正文或仅发送独立提示 | | items | array | 已筛选并排序的展示条目,不额外限制6条;当前检索仍有候选预算 | | sources | array | 完整来源,不一定与items一一对应;可能含仅供参考或分析未完成的来源 | | summary | object | 综合说明;独立summary事件与这里相同 | | follow_up_questions | string[] | 资格待确认问题列表,不是LangGraph中断 | | excluded_items | array | 未入选条目的source_id与reason,适合详情或诊断 | | presentation | object | 建议先显示items再summary;实际顺序以items数组/priority.rank为准 | | retrieval_analysis_complete | boolean | 本轮有限候选检索和逐项分析是否完整;不表示总体召回充分 | | complete | boolean | 在上项基础上考虑必要企业信息完整性等;不表示获批,且不包含所有降级状态 | | company_context | object | profile_verified、honors_complete和errors;不是完整企业画像 | | analysis_errors | array | 各候选分析错误,通常为record_id/code | | retrieval | object | 查询、失败库/查询/内容、快照等诊断;无候选全文数组 | | search_plan | object | needs、queries、error;needs每项有need/evidence/query,均为检索假设 | result.errors只包含会话/图层错误,不能替代recommendation.analysis_errors、retrieval、summary.status/error等字段。 ### 6.2 item:一张卡片及其证据 下面是政策介绍模式的合成item.data,展示完整顶层字段: ```json { "source_id": "k_000000000000000000000001", "title": "合成创业服务示例", "card": { "kind": "service", "name": {"text":"合成创业服务示例","citations":[{"path":"/title","quote":"合成创业服务示例"}]}, "department": null, "support_objects": {"text":"拟创业人员","citations":[{"path":"/raw_record/content","quote":"面向拟创业人员"}]}, "support_methods_standards": {"text":"提供创业咨询","citations":[{"path":"/raw_record/content","quote":"提供创业咨询"}]}, "application_conditions": null, "match_reason": { "text": "服务内容与咨询的创业主题相关。", "citations": [{"path":"/raw_record/content","quote":"提供创业咨询"}], "subject_policy_comparisons": [] } }, "match_basis": [], "match_reason": "服务内容与咨询的创业主题相关。", "answer": "这项服务面向拟创业人员,提供创业咨询。", "citations": [{"path":"/raw_record/content","quote":"面向拟创业人员,提供创业咨询。"}], "conditions": [], "eligibility": "not_evaluated", "recommendation_status": "相关政策与服务介绍", "priority": {"rank":1,"basis":"原检索顺序"} } ``` | card字段 | 展示内容 | | --- | --- | | kind | 新卡片固定policy;历史结果可能包含financial_product / service | | name | 完整政策、产品或服务名称,读取name.text | | department | 主管/实施部门或金融提供机构 | | support_objects | 支持对象 | | support_methods_standards | 支持方式及原文明示的标准 | | application_conditions | 原文申报条件,不等于用户符合条件 | | match_reason | 相关/匹配理由及主体—政策比较 | name及四个业务字段采用 `{text, citations}` 对象,不是字符串;后四项可能为null,可显示“原文未明确”或隐藏。当前入选items要求有有效card,前端仍应容错缺失字段。item.title是来源标题,展示正式名称优先使用card.name.text。 item.match_reason是字符串;card.match_reason是对象,含text、citations、subject_policy_comparisons。item.citations与card各字段citations中的path/quote指向该item.source_id对应的完整来源。path是记录内字段定位,不是HTTP地址。 priority.rank从1开始且数值越小越靠前;它不是分数或获批概率。不满足、明确过期、主体冲突和不适合推荐的条目会被筛出items,可查看excluded_items和sources。reason当前包括expired、ineligible、reference_only、subject_conflict、insufficient_personal_match、duplicate_name。 ### 6.3 具体需求的条件与匹配依据 以下为具体需求item的字段片段(并非完整item),展示数组元素结构: ```json { "eligibility": "待确认", "recommendation_status": "相关线索,资格待核验", "conditions": [ { "dimension": "主体", "status": "待确认", "quote": "面向拟创业人员", "user_evidence": "我想开一家小店" } ], "match_basis": [ { "dimension": "需求", "status": "supports", "fact_path": "/question", "fact_quote": "我想开一家小店", "policy_path": "/raw_record/content", "policy_quote": "面向拟创业人员,提供创业咨询。", "explanation": "创业咨询与用户开店准备需求相关。", "fact_origin": "user", "provenance": {"type":"user_statement"} } ] } ``` 入选item.conditions最多三项,不足不补齐;按关键申报门槛优先,优先使用申报条件引文,同等证据下待核验优先,保留模型重要性顺序。去除重复引文和无原文占位。每项仅dimension、status、quote、user_evidence四键,前端展示三部分:信息方向(dimension · status)、原文要求(quote)、已知信息(user_evidence)。不再返回requirement/question/evidence_path;补问仍使用next_actions/follow_up_questions。user_evidence为空时显示“暂无相关信息,待确认”。quote为完整保留限定的连续原文,不裁剪改写。维度为地区/主体/行业/时间/其他条件,状态为满足/不满足/待确认,模型“满足”仍保守转为“待确认”。完整分析与排除依据保留在candidate_diagnostics/excluded_items,不受三项展示上限影响。旧会话结果不回填。 match_basis维度为需求/地区/行业/资质/主体/时间/其他,状态为supports/conflicts/unknown;fact_origin为user/company/honor,provenance按来源可能有api_code、fetched_at、page_index。supports仅表示有关联依据,不表示全部条件满足;不要与conditions的中文状态混用。 **follow_up_questions不等于interrupt。** 前者是回答内容,通常仍done=completed;目前再输入答案会开始一个新问题,并未实现基于旧卡片的增量资格重算。需要补充时前端可让用户同时带上原问题和新条件。 ### 6.4 source:完整来源 source事件及recommendation.sources中的同一对象结构如下,full_record在此只展示部分字段: ```json { "id": "k_000000000000000000000001", "title": "合成创业服务示例", "type": "policy", "source": null, "full_record": { "id": "k_000000000000000000000001", "title": "合成创业服务示例", "raw_record": {"content":"面向拟创业人员,提供创业咨询。"} }, "snapshot": "0000000000000000000000000000000000000000000000000000000000000000", "analysis_complete": true, "detail_anchor": "source-k_000000000000000000000001" } ``` 实际full_record保留入库的完整原始字段,各知识类型结构不同,不能写死只有raw_record.content。source是原始来源对象或null;实际URL、文件等定位信息应按该对象展示,无URL时不要自行拼接地址。 source.id与item.source_id关联。detail_anchor是建议使用的页面锚点,不是后端详情接口,`/api/chat`也不会生成CLI的本地HTML报告。前端可以把full_record放入可展开来源详情,并按path/quote显示引文。snapshot为知识快照标识,不代表政策生效日期。 ### 6.5 summary:综合说明 ```json { "text": "本轮服务可供筹备创业时进一步了解,具体办理条件可查看原文。", "source_ids": ["k_000000000000000000000001"], "status": "generated", "notices": [] } ``` status为generated(模型生成)、fallback(生成失败使用备用说明)或no_recommendations(没有入选推荐)。fallback时可能有 `error: "summary_generation_failed"`;notices包含本轮不完整或同名来源处理等提示。summary.source_ids可用于跳转来源。**summary降级并不一定使recommendation.complete=false,需独立检查。** ## 7. 公司补问与继续请求 ### company_need interrupt.data示例: ```json { "kind": "company_need", "question": "本次是否需要结合具体公司情况?如需要,请补充公司名称或信用代码。", "schema": {"needs_company":"boolean","company_hint":"string"}, "input_help": "输入公司名称或信用代码;不需要则输入“不需要”。" } ``` 随后仍有result和 `done.data={"status":"needs_input"}`,result.response可能为空。前端应展示interrupt.question,不能把空response当作失败。 下一次POST仍只传两个字段: ```json {"thread_id":"window-20260914-001","question":"合成示例有限公司"} ``` 无需公司信息则question填“不需要”“无需公司信息”或 `/skip`。**schema是内部补问说明,不要将needs_company/company_hint作为额外HTTP参数提交。** ### company_selection 以下interrupt.data为部分字段示例: ```json { "kind": "company_selection", "question": "请选择您所指的公司;也可以补充名称重新检索或取消。", "result": { "candidates": [ {"KeyNo":"synthetic-company-key","Name":"合成示例有限公司","CreditCode":null,"Address":null,"Status":null} ], "may_have_more": false }, "page": 1, "input_help": "输入候选序号确认,或输入新关键词;支持/next、/retry、/cancel。" } ``` 实际还包含actions等信息;候选可能有StartDate、OperName、No,缺失值为null。result.source可携带来源与获取时间,查询失败时result.error为错误码且candidates可能为空。空候选与查询失败需分别展示。 | 用户操作 | 下一次question | | --- | --- | | 选择显示的第1家公司 | `"1"`,从1开始;不要重排候选后仍按原序号提交 | | 换关键词 | 新名称或代码 | | 下一页 | `/next`,仅may_have_more=true有效 | | 查询失败重试 | `/retry` | | 取消公司查询 | `/cancel`,继续以现有信息给出一般线索,不是取消HTTP | F038:question仍必须是字符串。同一个thread_id下,除序号外,也支持当前候选的准确全名、KeyNo、信用代码,或 `JSON.stringify(candidate)` 得到的JSON字符串。推荐前端直接使用 `question: selectedCompany.KeyNo`,避免展示顺序变化。也支持 `JSON.stringify({action: "select", key_no: selectedCompany.KeyNo})`。不要把对象直接赋给question。 后端只匹配该会话当前候选,JSON中的身份字段须一致且匹配唯一公司,CreditCode为null允许忽略;额外资料不会用于画像。重名或无效JSON选择保留确认状态,不重复搜索;普通新关键词仍用于重新检索。确认成功后恢复原咨询问题,继续工商、荣誉与政策检索,不将公司信息当成新的政策问题。存在补问时下一次输入用于回答补问,不能同时作为新咨询;要另问可换thread_id。 也兼容前端将候选显示字段用空白拼接,例如 `"合成示例有限公司 SYNTHETIC-CREDIT 存续 合成人员"`。整行每一项必须属于同一个当前候选,并包含全名、KeyNo或信用代码之一且唯一匹配;仅经营状态/法人、夹杂其他公司的代码或额外指令不会确认。无需为了这种已有显示格式改前端;新接入仍推荐直接传KeyNo字符串。 ## 8. 三种“失败/没有结果”不要混淆 ### 8.1 流开始前:HTTP错误 ```json {"error":"thread_busy"} ``` 这不是SSE,也不带统一thread_id/request_id封装;可从响应头X-Request-ID取得日志追踪ID。由代理或Uvicorn在进入应用前返回的错误不保证有该响应头。 | HTTP状态 | error | 说明 | | --- | --- | --- | | 415 | application_json_required | Content-Type不正确 | | 422 | invalid_request | JSON或thread_id/question/pictures/files字段不符合约束 | | 413 | request_too_large | 超过64KiB | | 408 | request_timeout | 请求体读取超时 | | 409 | thread_busy | 同窗口的上一请求还在执行 | | 429 | server_busy | 问答并发容量已满或正在关闭 | | 503 | service_unavailable | 无法提交后台任务 | 应用的409/429/503分支附 `Retry-After: 2`,只是建议重试间隔,不保证2秒后完成。Uvicorn、代理或网关也可能返回503/其它错误及纯文本,前端不能假设每个非200响应都有JSON。 ### 8.2 流开始后:error事件 ```text event: error data: {"thread_id":"window-20260914-001","request_id":"a78b17a3b38241fa83c8ee91f40ec608","data":{"code":"stream_timeout"}} ``` | code | 说明 | | --- | --- | | processing_failed | 本轮处理抛错,有通用message;不能据此认定没有相关政策 | | stream_timeout | 超过流持续时长上限,可能只有code,没有message | | unfinished_turn | 上轮有未完成的非补问节点;同ID输入/retry恢复,或换ID提出新问题 | 此时HTTP可能仍为200。error是终止事件,之后不保证有done。未收到done/error就断开,前端显示连接中断,不能假装回答已完成;不支持Last-Event-ID重放或按事件断点续传。 ### 8.3 正常结束,但业务降级或没有推荐 - `items=[]`、`complete=true`:可能没有对应依据,也可能全部条目被筛除;结合overview、sources和excluded_items解释,不说“政策不存在”。 - `complete=false`或analysis_errors非空:存在检索/分析/必要企业信息等缺口,不是正常无结果。 - 普通对话result.errors非空:可能是LLM失败后的兜底答复。 - summary.status=fallback:综合说明降级,卡片仍可使用已返回的证据。 `done.status=completed`只表示本轮流程结束,不能代替这些业务判断。满足条款、推荐排序、完整检索均不等于获批。 ## 9. 前端渲染与解析示例 可将progress/heartbeat放在独立状态栏;answer为正文或开场,item为卡片,summary为结尾。将source按id缓存用于引用详情。result是最终快照:可以用它替换之前拼装的数据,**不要把它再次追加成第二份回答/卡片**。 正文可能包含换行或Markdown,但没有单独format字段;纯文本安全展示即可,使用Markdown渲染时关闭或清理原始HTML。字段null不应显示成“null”,空数组不表示错误。不要将thread_id当作所有请求的唯一ID,回调时用request_id区分旧流与新流。 原生EventSource不能发送此JSON POST,可使用以下fetch解析器。它处理网络分块、UTF-8跨块、LF/CRLF分隔及非JSON HTTP错误;回调接收事件名和完整外层对象: ```javascript async function chat(threadId, question, onEvent, signal) { const response = await fetch('/api/chat', { method: 'POST', headers: {'Content-Type': 'application/json', 'Accept': 'text/event-stream'}, body: JSON.stringify({thread_id: threadId, question}), signal }); if (!response.ok) { let code = `http_${response.status}`; try { code = (await response.json()).error || code; } catch {} throw new Error(code); } if (!response.headers.get('content-type')?.includes('text/event-stream')) { throw new Error('unexpected_content_type'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = '', terminal = false; try { while (true) { const {value, done} = await reader.read(); buffer += done ? decoder.decode() : decoder.decode(value, {stream: true}); let boundary; while ((boundary = /\r?\n\r?\n/.exec(buffer)) !== null) { const block = buffer.slice(0, boundary.index); buffer = buffer.slice(boundary.index + boundary[0].length); const lines = block.split(/\r?\n/); const event = lines.find(line => line.startsWith('event:'))?.slice(6).trim(); const data = lines.filter(line => line.startsWith('data:')) .map(line => line.slice(5).replace(/^ /, '')).join('\n'); if (!event || !data) continue; const payload = JSON.parse(data); onEvent(event, payload); if (event === 'done' || event === 'error') terminal = true; } if (done) break; } if (!terminal) throw new Error('stream_disconnected'); } finally { try { await reader.cancel(); } finally { reader.releaseLock(); } } } ``` onEvent须处理error事件;函数正常解析到error不自动抛出服务错误。AbortController可以中止前端连接,但后台已发出的同步调用不能强制取消,会话锁保留到执行线程真正结束,立即重发可能409。新问题不要无条件自动重试,以免重复生成或外部调用。 ## 10. 当前接入限制 当前单进程运行,同会话排他;默认同时16个问答,超额拒绝,不保证真实业务QPS。默认流超时600秒。会话保存在Redis,默认写入后15天过期;新问题重置本轮主体,未实现普通聊天历史理解。ID不存在/过期不会返回404,而是新建状态。 尚未实现跨进程会话锁、用户登录/归属校验、跨域CORS、历史列表或删除接口;前端通常通过同源代理接入。反向代理需关闭响应缓冲并配置合理超时。本接口没有新增天气、实时新闻或办理进度查询能力。 ## 11. 实现对照 - [HTTP校验、SSE封装与容量控制](../src/step4_api/app.py) - [事件顺序、最终结果与会话恢复](../src/step4_api/runtime.py) - [阶段提示](../src/step4_api/progress.py) - [推荐与来源字段](../src/step3_question_answer/recommendation.py) - [卡片及综合说明](../src/step3_question_answer/cards.py) - [主体与政策比较](../src/step3_question_answer/matching.py) ## 知识正文统一归纳(F047) 非同题QA的多来源、多块或长回答统一生成summary.text,混合卡片不再先生成总结后追加知识回答。默认目标150–350字,上限600字;明确详细/完整/原文请求上限1800字,不硬截断。单条350字内无换行答案直接保留;同题QA仍按既有原文/冲突规则展示。summary.source_ids与citations关联实际采用来源;完整sources、knowledge_answers和卡片保留,不应再把knowledge_answers追加到正文。归纳失败为status=fallback、error=summary_generation_failed,正文明确说明整理失败,原文仍可查看。接口字段不变;源ID合法和引文逐字存在不保证归纳语义完全正确。 ### F047 Markdown精简(2026-09-16 18:04) 最终正文短结论开场,简单问题可无标题;通常1–2个三级短标题、最多3个,步骤编号、材料/条件列表,标题和列表前后空行,局部加粗。不强凑栏目、重复来源或泛化结尾。下一步建议保留既有三项结构,正文紧凑显示;必要补问保留理由,通用理由仅在next_actions.reason保留。旧示例中每项后另起一段通用理由的展示方式已替代。来源、同题QA原文及接口字段不变;模型仍可能少量重复,外部前端视觉效果未验收。 ### F047取消字数限制(2026-09-16 18:09) 按用户最新要求,最终统一回答、中间证据笔记和政策综合说明不再用字符数拒绝,也不按长度额外重试或截断。原600/1800/2000字说明作废;提示词仅要求精简、美观、去重并保留关键条件。summary.diagnostics不再产生text_limit,output_chars仅作观测。输入上下文预算、模型输出token上限、分批停止条件及来源/结构校验仍保留。此修复针对1046字被错误丢弃的已定位原因,需重启后新请求验证。 # F049诊断补充(2026-09-17) F052(2026-09-17):入选政策item.recommendation_status现为最高扶持/支持标准短文案,尽量10字内, 不再作为资格状态展示。原状态文案移到eligibility_status;eligibility和conditions不变。 新增support_highlight={text,citations}用于短文案来源追溯;可继续直接展示recommendation_status, 例如“最高100万元”“最高20万元/年”,未明确金额时为支持方式或详情提示。不可将金额标签解读为已符合资格。 F050(2026-09-17):企业地区差异按拟在青浦发展场景待核验,相关政策不单因当前外地地址被剔除。 原follow_up_questions和next_actions均在截取数量前优先地区补问,summary.text下一步建议同步。 相关item新增regional_context=qingpu_development,其eligibility为待确认(其他明确不满足仍排除), recommendation_status和summary突出落地后的适用前提。分公司/迁址不是已符合资格的承诺。 最终 `result.data.recommendation.candidate_diagnostics` 保存全部召回候选的处理结果和 通过校验的逐块分析依据;`excluded_items` 的卡片筛选排除项新增具体条件、用户证据、 原文引用和匹配依据。详细字段见 [第三步F049](step3-agent.md)。这些是诊断字段, 不要求前端新增正文或卡片展示;原 `source_id/reason`、items、summary字段兼容。 新请求随会话检查点保存,旧请求不回填;终端只输出 `candidate_outcome_counts` 计数。 2026-09-18字段更正:对外result.data只返回company_info,不再返回compang_info,前端需同步。内部图状态/Redis保留旧键compang_info,接口读取后映射为company_info,兼容已有检查点且无批量迁移。历史已保存的问答结果不回填。未查询时仍为{},company/profile/honors内部结构不变。