AAIPROS

AIPROS · Static Essay Page

花了10天搭了个AI平台,踩了10个坑才上线——一个技术人的架构复盘

Agent治理 公众号文章 2026-04-30 4 min

最近两周,我把自己关在电脑前,从零到一搭了一个叫 Skill Platform 的产品——一个企业级 AI Agent 治理平台。

最近两周,我把自己关在电脑前,从零到一搭了一个叫 Skill Platform 的产品——一个企业级 AI Agent 治理平台。

说"从零到一"不完全准确。架构思想不是新的,代码也不是全部自己敲的——有一半是跟 AI 结对编程出来的。但整个过程中,我经历了产品设计的推敲、架构分层取舍、部署踩坑、GitHub 文件损坏、移动端兼容、AI 模型输出不稳定……几乎把一个小型全栈项目能踩的坑都踩了一遍。

这篇文章把整个过程完整复盘一遍。不是为了炫耀什么——这个产品本质上只是一个 Demo,验证架构能力的,集成到企业还要二次改造。但我想,这样真实的过程记录,比那些"30天造一个AI平台"的营销文,对从业者更有参考价值。

这个东西到底是干什么的

先一句话说清楚产品形态。

Skill Platform 是一个"流程驱动 AI"的智能体治理平台。

核心链路是这样的:

1 用架构树定义业务节点(比如"合同审批"、"发票校验") 2 每个节点关联具体的流程文档 3 AI 根据流程内容自动规划需要哪些 Skill 4 用户创建自定义 Agent,关联这些 Skill 5 和 Agent 对话,它自动调用对应能力

你也可以理解为:这是一个简化版的"AI 中台"——但不是传统意义的 API 网关那种中台,而是把"流程分析 → 能力识别 → Agent 编排 → 人机协作"串起来的平台。

架构设计:为什么这么分

这个项目分了 三层,彼此独立部署、独立演进。

第一层:前端(React + TypeScript + Ant Design)

坦白说,前端不是这个项目的核心亮点。但有个设计我觉得值得讲讲—— 产物驱动的对话交互。

传统的 AI 对话页面是什么样的?左边聊天框,右边一片空白,或者什么都没有。你问一句它答一句,除了文字就是文字。

我们的对话页面不一样。AI 回复的时候,如果内容里包含代码块、表格,这些内容不会被"埋"在对话流里——它会被自动识别出来,变成一张"产物卡片",用户点击卡片就可以在右侧的 Canvas 里展开查看、预览、复制。

这不是多复杂的技术。核心逻辑就两个正则表达式:一个匹配代码块 ```,一个匹配 Markdown 表格 |---|。但我认为这个交互范式是对的——对话只是交互的入口,产物才是价值的载体。

另一个让我花了不少心思的地方是 响应式适配。我做了移动端版本,但有一个"铁律"——不能影响桌面端。解决方案其实很朴素:在 MainLayout 里用 Ant Design 的 useBreakpoint 判断屏幕宽度,小于 768px 就走独立的移动端渲染分支,完全不走同一套 CSS。

移动端导航从固定侧边栏变成了 Drawer 抽屉,Agent 卡片从网格变成竖排堆叠,聊天页面全屏沉浸。代码上就是 if (isMobile) { return } 一句话的问题——但这么一句,让我踩了两个坑(后面讲)。

第二层:后端(NestJS + TypeORM + SQLite)

后端是整个平台的"大脑"。18 个实体模型覆盖了 Tenants、Users、Skills、Agents、KnowledgeBases、Memory 等完整的业务域。

这里最有意思的设计是 AI 服务的双路径架构。

AI 规划 Skill 的场景是这样的:用户告诉系统"我有一个合同审批流程",系统调用大模型,返回一个结构化的 Skill 列表。如果模型返回了规范的 JSON,万事大吉。但问题是——模型不是总能稳定输出 JSON,尤其是用兼容接口调用阿里云通义千问的时候。

所以我设计了两个路径:

路径 A:结构化输出。用 OpenAI SDK 的 json_schema 模式,告诉模型"你必须输出这个 Schema 的 JSON Array"。这是首选路径。

路径 B:传统正则解析。如果路径 A 失败——不纠结,直接 fallback 到旧方式,用正则从模型回复里抠 JSON 数组。

这个设计的核心思想就一句话: 让 AI 做它擅长的事,不要让它做它不擅长的事。模型擅长的是理解业务流程、生成内容,不擅长的是严格遵守格式规范。所以我们在系统层面做校验和归一化,而不是指望模型一次就做对。

对话系统也用了一些设计心思。SSE 流式输出、消息历史管理、最近 20 轮截断、agentId 动态注入系统提示词——这些都不是什么新技术,但拼在一起形成了一个可用的对话体验。还有一个让我觉得挺实用的功能:AI 回复可以一键导出 Word 文档,后端用 npm docx 库把 Markdown 转换成 .docx 文件。

第三层:Agent Runtime(Python FastAPI)

这一层是一个独立的 Python 服务,通过 HTTP 和 NestJS 后端通信。它负责实际执行 AI 推理和工具调用。

设计上有几个取舍值得说:

一是 Skill 加载系统。每个 Skill 本质上是 skills/ 目录下的一个文件夹,里面有 skill.yaml(元数据)和 SKILL.md(核心指令)。运行时扫描目录、动态加载。这套规范参考了 Anthropic 的 Skill 标准,但做了一些简化。

二是 会话持久化。一开始只有内存存储,重启就丢。后来加了 JSON 文件持久化,虽然不适合生产,但对 Demo 来说足够。

三是 V2 对话端点。因为 Deep Agent SDK 在部署环境没装上,所以我加了一个 /v2/chat 端点,直接调用百炼 API,不经过 Agent Runtime 的编排层。这不是最优解,但保证了"线上能用"。

踩的坑:10 个,一个一个说

以下按时间顺序,每一个都是真金白银换来的。希望对你有帮助。

坑 1:GitHub 文件被截断

推代码到 GitHub 后,发现 ai.service.ts 这个文件变成了 364 字节——只有前 6 行。AgentChatCanvas.tsx 更离谱,只剩下 "..." 三个字符。排查了半天,发现是 HTTP/2 协议在推送大文件时连接不稳定,导致文件传输中断。最终用 GitHub MCP 的 push_files 工具逐个文件推送解决的。

教训:大文件推送不要依赖 git push 一次搞定,尤其是网络不稳定的环境。

坑 2:Git Push 卡在 1%

第一次 push 整个项目到 GitHub,进度卡在 1% 不动了,等了 5 分钟报错 "RST_STREAM"。尝试了增大缓冲区、用浅克隆都没用。最后加了一个环境变量绕过 HTTP2: git -c http.version=HTTP/1.1 push。直接一行解决问题。

教训:GitHub 在国内的网络环境,HTTP/1.1 比 HTTP/2 稳得多。

坑 3:对话历史重启就丢

Agent Runtime 的会话管理一开始全部在内存里。服务重启、重新部署,用户的对话记录全部丢失。后来给 session_manager.py 增加了 JSON 文件持久化,每次写入都会序列化到磁盘。

教训:Demo 也要考虑"如果服务重启了怎么办"。

坑 4:setMobileNavOpen 未定义

在 MainLayout.tsx 里加移动端导航逻辑,调用 setMobileNavOpen(false) 时 JS 报错说这个函数不存在。原因是我把 state 的声明放在了后面,调用在前面。JavaScript 没有"提升"函数组件的 useState 声明。把 useState 移到使用之前就解决了。

教训:Hooks 的调用顺序在 React 里是最重要的规则。

坑 5:JSX 标签不匹配