标签: 前后端联调 / CORS 跨域 / SSE / 大模型容错 / 边缘测试 / AI 原生开发

一、 架构的丰满与联调的骨感

经历了前期高歌猛进的基础建设与核心算法编写,我们的《用药安全决策引擎》终于迎来了最激动人心的环节:双端联调。

纸面上的架构图总是优雅完美的——前端发起请求,后端 Agent 调用图谱,数据如丝般顺滑地流转。然而,当后端 FastAPI 接口与前端 Vue3 真正开始数据握手时,我们一脚踏进了“深水区”。跨域阻断、流式协议的中断、大模型间歇性的 JSON 幻觉接踵而至。

作为架构师,解决“不确定性”是我的核心职责。在这篇手记中,我将复盘我们在联调阶段遭遇的三大经典暗坑,以及我们是如何用工程化手段将其逐一化解的。

二、 暗坑一:SSE 流式推送下的 CORS 跨域梦魇

在【手记(六)】中,为了实现 ReAct 工作流的可视化,我们引入了 SSE(Server-Sent Events)协议。但当我们把前后端分别跑在不同端口(如前端 localhost:5173,后端 localhost:8000)时,标准的 HTTP 跨域配置失效了。

问题表象: 普通的 POST 请求在配置了 FastAPI 的 CORSMiddleware 后畅通无阻,但原生的 EventSource 发起 SSE 请求时,浏览器却频频报跨域错误,甚至无法发出 OPTIONS 预检请求。

排障与破局: 通过抓包分析,我们发现原生 EventSource 不支持自定义 Header 且在跨域策略上极为严苛。为了不妥协于降级方案,我们最终采取了双管齐下的策略:

  1. 前端改造:放弃原生 EventSource,引入支持更完善的 @microsoft/fetch-event-source 库,使其能够携带鉴权 Token 并完美兼容 fetch 的跨域规范。

  2. 后端放行:在 FastAPI 的跨域中间件中,除了暴露常规的 Headers 外,明确允许 Accept: text/event-streamCache-Control: no-cache,彻底打通了流式数据的传输管道。

三、 暗坑二:大模型 JSON 输出的“间歇性抽风”

尽管我们在 Prompt 中三令五申,甚至开启了 API 的 JSON Mode,但在高并发压测下,大模型依然会在 3% 的请求中发生“JSON 崩溃”。

边缘场景(Edge Cases)复现

  • 场景 A:当解释药理机制时,模型输出了未转义的特殊字符(如换行符 \n 或英文双引号),导致前端 JSON.parse() 直接抛出致命异常。

  • 场景 B:模型画蛇添足,在标准的 JSON 字符串外层包裹了 Markdown 的 json 代码块。

防御性编程(Defensive Programming)落地: 系统绝不能因为一个字符的转义错误就让医生无法开药。为此,我们在后端的 LangChain 输出解析层,建立了一道强大的“容错过滤网”。我们充分利用了 AI 原生开发流,借助 Cursor 和 DeepSeek 快速推演并生成了十几种不同损坏程度的 JSON 恢复用例,最终编写了一套基于 AST(抽象语法树)思想和正则兜底的健壮解析器:

Python

import re
import json

def robust_json_parse(llm_output: str) -> dict:
    # 步骤 1:暴力剥离 Markdown 代码块
    cleaned_str = re.sub(r'^```json\s*', '', llm_output)
    cleaned_str = re.sub(r'\s*```$', '', cleaned_str)
    
    try:
        # 步骤 2:尝试标准解析
        return json.loads(cleaned_str)
    except json.JSONDecodeError as e:
        # 步骤 3:Fallback 容错——尝试利用大模型进行自我修复(Auto-Retry)
        # 或利用本地预编译的容错正则引擎强行提取关键字段
        return trigger_fallback_parsing(cleaned_str)

四、 暗坑三:Vue3 响应式状态在流式数据下的丢失

这个问题极为隐蔽。当前端接收到 SSE 源源不断的字符串时,我们需要不断拼装这些字符串,并驱动 UI 重新渲染。

一开始,前端同学简单地通过不断重新赋值整个 reactive 对象来更新状态(例如 state.value = parsedData)。但这在极短时间的高频推流下,导致了 Vue3 内部 Proxy 代理对象的引用断裂,页面出现了“静默失败”——数据在 Console 里打印出来了,但 UI 死活不更新。

状态机稳固策略: 我们重新审视了 Vue3 的响应式原理,将高频流式数据的变更逻辑收口至 Pinia 的 Action 中。摒弃了粗暴的整体赋值,改为基于属性的深层修改(Deep Mutation),并在更新关键节点时合理利用 nextTick,确保浏览器的渲染帧不会被高频的 JS 执行所阻塞。

五、 结语:工程即妥协与兜底的艺术

回头来看,这半个多月的联调经历,给团队上了最生动的一课。真正的软件工程,从来不是温室里的乌托邦,而是充满了网络抖动、数据脏乱和状态失步的泥潭。优秀的架构,其价值不不仅在于顺风局跑得多快,更在于逆风局下的容错、兜底与降级能力。

伴随着这几个核心暗坑的填平,我们的《用药安全决策引擎》终于跑通了全量测试用例。在下一篇,也就是本系列博客的最终篇【架构手记(八)】中,我将站在项目即将开源与交付的节点,做一次深度的架构复盘:从 Demo 到医疗 SaaS 原型,我们究竟做对了什么?又妥协了什么?

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐