项目是什么
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 依次发送 thinking、tool_start、tool_retry、tool_done、delta、done 等事件。
当模型请求后端工具时,后端会校验参数并执行;遇到可恢复的限流、连接失败、服务暂不可用或超时时,会按固定次数和退避策略自动重试。工具结果会作为下一轮模型上下文的一部分,直到模型输出最终答案。
对 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 应用,或者对其中某个实现细节有想法,欢迎交流。