AI Chat:从流式聊天到 Workspace Agent,我做了一个可恢复的 AI 应用

React + FastAPI + DeepSeek:把流式对话、工具调用、本地工作区审批和任务恢复串成一个完整闭环

Posted by franki on September 2, 2026

项目是什么

AI Chat 是我做的一个全栈 AI 应用。它从一个最小的流式聊天 Demo 开始,后来逐步补齐了会话管理、Markdown 与公式渲染、模型与思考模式选择、工具调用、网页搜索引用,以及一个更有意思的 Workspace Agent。

Workspace Agent 让模型可以协助修改用户选择的本地项目文件,但不会把本机目录权限交给后端或模型:浏览器负责读取和写入,后端负责规划、生成、审查和流式编排,用户对每一个文件拥有最终确认权。

项目仓库:https://github.com/NikFranki/ai-agent-learn/tree/main/ai-chat

在线体验:https://ai-chat-blue-gamma.vercel.app


目前实现了什么

聊天体验

  • 多会话创建、切换、重命名、搜索、导入和导出
  • 流式输出、停止生成、失败重试、编辑问题后重新生成
  • Markdown、表格、代码块和 KaTeX 数学公式渲染
  • 深色/浅色主题、常用提示词收藏和复用
  • 每个会话独立选择 V4 Flash / V4 Pro,以及快速回答 / 深度思考模式
  • 长对话自动压缩上下文:保留最新消息原文,把早期信息整理成摘要后再发送给模型

聊天记录保存在浏览器 IndexedDB,而不是后端数据库。这样刷新页面后仍能恢复会话,也避免把普通对话内容存到服务端。

Tool Calling 与联网能力

模型可以调用受限的后端工具:

  • 计算器
  • 当前时间
  • 天气
  • 汇率换算
  • 网页搜索

工具调用不是“模型说要调就直接执行”。后端会做参数校验、超时控制、重试、结果裁剪和结构化日志。网页搜索还会做 URL 规范化、去重、排序、来源域名多样性处理,并在答案下展示来源卡片。

Workspace Agent

这是项目里我投入最多的部分。它支持:

  • 在浏览器中选择一个本地目录,并申请读写权限
  • 扫描有限深度的目录树和项目清单
  • 让模型提出要修改哪些文件、每个文件的目的和验收条件
  • 后端按计划批量生成候选文件内容
  • 在界面中展示每个文件的用途、完整内容和 Diff
  • 用户逐文件同意、拒绝,或批量处理;真正写文件只在浏览器发生
  • 将任务、候选草稿、审批记录和工作流检查点保存下来,刷新后仍可继续处理

它不提供删除文件、Shell、Git、安装依赖或任意网络访问。这些能力的风险更高,先把“生成—审核—写入”的基本闭环做稳更重要。


前端和后端怎么选型

前端:React + TypeScript + Vite

前端使用 React、TypeScript 和 Vite。App.tsx 负责页面状态机和 SSE 请求编排,其他职责拆到独立模块:

  • storage.ts:IndexedDB / localStorage 的持久化与迁移
  • workspace.ts:路径、计划和浏览器文件操作的安全校验
  • validators.ts:导入聊天记录与工具数据的结构校验
  • components/:工具状态、来源卡片、Agent 交接、文件 Diff 与审批 UI

浏览器端使用 File System Access API 获取用户明确授予的目录句柄。它特别适合这个场景:后端不需要、也不应该拥有用户电脑的文件权限。

后端:FastAPI + OpenAI 兼容接口

后端用 FastAPI,负责认证、SSE 流、模型调用、工具执行和工作流状态。模型层使用 OpenAI 兼容接口接入 DeepSeek,并通过一个 ModelAdapter 隔离调用细节,减少后续替换 Provider 的成本。

后端目录按职责拆分:

app/
├── chat/       # SSE、上下文、模型适配、意图路由
├── tools/      # 工具 Schema、执行、搜索排序与重试策略
├── workspace/  # Workspace 协议、内容生成、任务状态
└── core/       # 配置、认证、Pydantic Schema、日志

线上部署为一个 Vercel 项目:Vite 构建前端静态文件,api/index.py 作为 FastAPI Python Function。前端和 API 同源,开发时则由 Vite 代理 /api 到本地 FastAPI。


整体架构图

这个项目的核心原则是:对话和目录权限归浏览器,模型编排和外部工具归后端,Workspace 的有限恢复状态单独保存。

                              ┌───────────────────────────────┐
                              │            用户               │
                              └───────────────┬───────────────┘
                                              │
                                              ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ React + TypeScript + Vite(浏览器)                                      │
│                                                                         │
│  会话 / 摘要 ───────────────► IndexedDB                                 │
│  主题 / 收藏提示词 ──────────► localStorage                             │
│  Workspace 目录句柄 ─────────► File System Access API                   │
│                                                                         │
│  SSE 客户端、工具状态、Diff、逐文件审批                                │
└───────────────────────────────┬─────────────────────────────────────────┘
                                │ HTTPS + Cookie + SSE
                                ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ FastAPI                                                                  │
│                                                                         │
│  认证 │ 上下文压缩 │ SSE 编排 │ 意图路由 │ 工具校验 / 重试 / 超时       │
│       │ Workspace 计划、批量生成、审查、任务状态                        │
└───────┬─────────────────────────────┬───────────────────────┬───────────┘
        │                             │                       │
        ▼                             ▼                       ▼
┌───────────────┐             ┌───────────────┐       ┌───────────────────┐
│ DeepSeek      │             │ 固定后端工具  │       │ SQLite + LangGraph│
│ 流式模型接口  │             │ 时间/计算/    │       │ Workspace 任务、  │
│               │             │ 天气/汇率/搜索│       │ 草稿、审计、检查点│
└───────────────┘             └───────────────┘       └───────────────────┘

这里有两个刻意保留的边界:FastAPI 不直接接触用户本地目录;SQLite 也不保存普通聊天内容,只保存可恢复的 Workspace 任务与候选草稿。


关键实现:如何把 Agent 跑起来

1. 流式聊天与 Tool Calling 循环

前端向 /api/chat 发起请求后,后端通过 SSE 依次发送 thinkingtool_starttool_retrytool_donedeltadone 等事件。

当模型请求后端工具时,后端会校验参数并执行;遇到可恢复的限流、连接失败、服务暂不可用或超时时,会按固定次数和退避策略自动重试。工具结果会作为下一轮模型上下文的一部分,直到模型输出最终答案。

对 Workspace Agent 而言,client_tool_request 是一个重要的边界:后端发出浏览器工具请求后主动结束本轮 SSE,浏览器完成本地读取或用户审批后,再带着观察结果开始下一轮请求。这样不需要 WebSocket,也不会让 FastAPI 直接碰到本地文件。

下面是一次典型的“修改本地项目文件”流程:

用户             浏览器 SPA                 FastAPI / 模型              本地目录
 │                   │                           │                         │
 │ 选择目录并提需求  │                           │                         │
 ├──────────────────►│                           │                         │
 │                   │ POST /api/chat            │                         │
 │                   ├──────────────────────────►│                         │
 │                   │                           │ 模型请求 inspect       │
 │                   │ client_tool_request + done│                         │
 │                   │◄──────────────────────────┤                         │
 │                   │ 扫描受限目录 / 清单                                  │
 │                   ├────────────────────────────────────────────────────►│
 │                   │◄────────────────────────────────────────────────────┤
 │                   │ POST /api/chat + 扫描结果 │                         │
 │                   ├──────────────────────────►│ 生成文件计划           │
 │                   │◄──────────────────────────┤                         │
 │                   │                           │ 批量生成候选内容       │
 │                   │◄──────────────────────────┤                         │
 │ 查看 Diff,逐文件同意 / 拒绝                    │                         │
 ├──────────────────►│                           │                         │
 │                   │ 同意后才通过目录句柄写入                            │
 │                   ├────────────────────────────────────────────────────►│
 │                   │                           │ 保存草稿状态 / 审计     │
 │                   ├──────────────────────────►│                         │
 │                   │ 完成状态与最终回答         │                         │
 │                   │◄──────────────────────────┤                         │

流程中模型输出的是“计划”和“候选内容”;真正的本地读取、写入和最终审批始终由浏览器及用户掌握。

2. 本地文件权限与人工审批

浏览器拿到目录句柄后,只接受相对路径;绝对路径、.. 穿越、敏感文件名、二进制文件和超大文件都会被拒绝。模型永远看不到绝对路径。

生成内容后,应用不会自动写入。用户可以逐个查看 Diff,再选择同意或拒绝。只有同意后,浏览器才会通过目录句柄写入对应文件。这个设计把 AI 的产出定位为“候选修改”,而不是未经确认的自动操作。

3. 可恢复的任务状态

普通聊天历史留在浏览器;Workspace Agent 则额外使用 SQLite 保存有限的任务状态:任务阶段、计划、候选草稿、内容哈希、版本、审批审计和 LangGraph checkpoint。

这么做不是为了把所有聊天都放进数据库,而是为了让用户刷新页面或后端重启后,仍能找到一个正在审核的文件变更任务。任务和草稿也有容量与 TTL 限制,避免本地状态无限增长。

4. 批量生成与多 Agent 交接

当前工作区流程会把已计划的一组文件交给后端生成,再由浏览器验证返回的路径是否与计划一致,并拆成逐文件审批卡片。

在需要时,系统会形成 Analyst、Coordinator、Reviewer 之间的有界交接信息:项目快照、实现说明和变更审查。交接内容会作为下一轮的受限上下文,而不是模型可以执行的指令;原始文件仍需要通过只读工作区工具重新获取。


我在这个项目里学到什么

这次不只是“调通一个聊天接口”,而是让我真正梳理了一遍 AI 应用的工程问题。

Agent 不是一句 Prompt

一个可用的 Agent,至少需要明确的状态、工具协议、输入输出校验、失败处理、人工确认和可观测性。模型能力很重要,但稳定的边界更重要。

权限边界要先于功能扩张

本地文件操作如果直接交给后端或模型,风险会迅速放大。把文件能力留在浏览器,把执行确认留给用户,虽然流程多了一轮,但职责和风险都更清楚。

流式体验背后是状态机

SSE 不只是“不断把文字 append 到页面”。工具开始、重试、暂停、浏览器执行、恢复、超时、中断和最终完成,都需要前后端对事件顺序达成一致。

评测、日志和性能基线要尽早补

项目里加入了离线 Tool Calling Eval、单元测试、结构化日志和合成性能基准。它们不能替代真实用户反馈,但能让重构和扩展时更有底气,也更容易定位问题。


接下来还能继续突破什么

我觉得这个项目还有不少值得探索的方向:

  • 为 Workspace Agent 增加真正的代码执行沙箱,用于测试、构建和静态检查
  • 把持久化任务状态从本地 SQLite 扩展到适合生产环境的托管数据库
  • 引入更细粒度的权限策略,例如只读模式、目录级授权、命令白名单和团队审计
  • 增加更真实的端到端评测,覆盖模型质量、任务成功率与人工审批体验
  • 支持更多模型 Provider,比较速度、成本、工具调用稳定性和推理质量
  • 尝试更多垂直 Agent:代码审查、知识库整理、数据分析、跨境电商运营辅助等

其中最值得谨慎推进的是 Shell 和代码执行能力。它们确实能让 Agent 从“会写文件”走向“能验证结果”,但必须先有隔离容器、网络与资源限制、受控挂载目录,以及清晰的用户确认机制。


写在最后

AI Chat 目前还不是一个完成品,但它已经让我从前端交互、后端编排、模型接口、工具协议到安全边界,完整走了一遍 AI Agent 应用的开发过程。

接下来我希望继续做更多 AI Agent 应用,把学到的东西真正用在具体问题上。也很期待认识同样在学习、折腾和实践 AI 应用开发的朋友:一起交流、互相 review、共同进步。

如果你也在做 Agent、全栈 AI 应用,或者对其中某个实现细节有想法,欢迎交流。