本篇覆盖 P3a 用户端对话模块(A/B/C/D 四切片全上线)一次把整个项目定位重新想清楚的架构校正。 如果说 (一) 是「把一根线通到公网」,(二) 就是「往线上挂第一个真产品,并在挂之前先停下来问:这条线到底是给谁用的」。

P3a — 第一个「应用」:一个对话产品

P2 之后底座能跑了,但底座不是产品。P3a 是我在这套底座上造的第一个面向人的东西——一个对话应用。 刻意做薄:只做「对话」这一个模块,登录 + 多轮聊天 + 选 agent,不贪大。

前端栈定成 Vite SPA + Vue3 + shadcn-vue(Reka UI + Tailwind)+ PWA + Pinia, 审美北极星是 LobeChat;双主题(青碧 / 电光青,全走 CSS 变量)。登录后就是个 app,SSR 用不上。 后端沿用底座:网关加了白名单账号密码登录(argon2 + 防枚举等时校验 + 每 IP/账号限流), 会话是服务端 chat_sessions 多轮,上下文用滑动窗口(~3000 token 或 20 条,先到先停), session_id 是客户端生成的 UUID,家侧先写消息成功、网关再 lazy 登记会话(避免孤儿)。

这个模块我拆成四个切片依次上线,每个都留了坑给我长记性:

  • A · 生产上线。把 web 构建进云端 Nginx、网关部署上云、库加列、种正式账号。 加了「真 per-IP 限流」(只在直连对端是回环、且显式开启信任时才采信 nginx 追加的 X-Forwarded-For 最右段, 否则用 RemoteAddr——默认关,不给伪造留缝)。
  • B · 内容审核真做。不接第三方,自建关键词/规则:规则进 DB 表,输入前置 + 输出分段两个切点真拦, 命中回 event:error{内容不合规}不泄露命中词GATEWAY_MODERATION=off 是整体逃生开关, 有 DB 就默认开、加载失败直接 fatal(fail-closed)。词表不进 git
  • C · 集成小修。修了几个只有真用才暴露的集成缺陷,最有代表性的是前端错误态串会话: 流式回调原来按「位置下标」写 messages[idx],而切换历史会话时整个数组被替换, 迟到的回调就写到别的会话去了——改成捕获消息对象的响应式引用再写,收尾再校验同会话。
  • D · 管理端 v1。只做「登录 + 只读服务健康看板」,但前端拉了一整套 naive-ui-admin 脚手架 (为后续管理模块留地方)。网关加 /v1/admin/health 并发探测各组件(网关 / 元数据库 / 家侧后端 / agent 计数), 单项失败不整体 500,总超时 3s。

坑 · systemd 不继承你的登录 shell 环境。 Gemini 走 generativelanguage.googleapis.com,在 systemd 跑的后端进程里直连被墙卡 60s 超时, 而我排障时在终端里 curl 每次都通——因为我的登录 shell 有 HTTPS_PROXY(Clash), 而 systemd 服务根本不继承登录 shell 的代理变量。用 httpxtrust_env 开关一对照实锤: 走代理 4.6s OK / 直连 ConnectTimeout。修法是把代理写进服务自己的 env 文件。 这个坑的教训比坑本身值钱:「我手动能跑通」和「服务能跑通」是两个环境,别用前者证明后者。

坑 · jsdom 单测 + curl 冒烟,结构性地测不到两类东西。 一是浏览器 CORS 预检:前端流式 POST 带自定义头触发 OPTIONS 预检,网关没放行该头 → 浏览器直接拦, 而 curl 根本不发预检,冒烟全绿却骗了我。二是真实布局:H5 用户端的侧栏在手机上把聊天区挤垮, jsdom 和桌面宽度都测不到。结论写进了我的验收规范:面向移动端的产品,必须真机窄屏过一遍

然后我停下来,问了一个更上层的问题

P3a 交付完,下一步本该是给它加个飞书渠道(P3b)。但动手前我问了自己一句: 我的 P4 要做的那些应用,真的都是「聊天」吗?

不是。我后面要做的是多个独立的产品,每个有自己的特色能力, 接大模型只是其中一环、一个特色,不是全部。想清楚这件事,整个底座的定位就得重新校准—— 而校准的结果,让我发现自己之前把它想窄了。

岔口是:agent-os 到底是什么?

  • 是一个平台(apps 作为租户长在它上面、共享它的一切运行时)?
  • 还是一个共享服务(多个独立产品各自全栈,只在需要时来调它)?

我的答案是后者,但要更精确:agent-os 是一层「共享控制面」——

  • 一个所有产品共用的统一网关(总入口:鉴权 / 限流 / 路由 / 观测 / 审核 / 用量),验证阶段大家都从这进;
  • echo / dify 是集中的 LLM 接入层:各产品有各自的「特色 agent」,但都经这一层去对接大模型, provider 密钥、代理、RAG 全集中在这(产品不各自直连大模型);
  • 一个跨产品的 admin,是我这个开发者的观测台,按产品维度看各家的流量 / 用量 / 健康度;
  • 而每个 toC 产品,自持前端 / 后端 / 数据库 / 身份 / 计费 / 业务,部署在网关后面。
flowchart TB
    U["各产品的终端用户"]
    subgraph APPS["多个独立产品"]
      FA["产品A 前端"]
      FB["产品B 前端"]
      FC["产品C 前端 …"]
    end
    U --> FA & FB & FC
    FA & FB & FC -->|"验证阶段:先经统一网关"| GW
    GW["统一网关(共享控制面)<br/>鉴权 / 限流 / 路由 / 观测 / 审核 / 用量<br/>▸ 按产品 URI 隔离"]
    GW -->|"反代(待建)"| BE["产品 各自后端 + DB<br/>身份 / 计费 / 业务 / 特色 agent"]
    GW -->|"AI 那一环"| AI["echo / dify<br/>集中 LLM 接入层"]
    AI --> LLM["外部大模型"]
    GW -.->|"旁路只读"| ADM["跨产品 admin<br/>开发者观测台"]

这一校准最锋利的推论有点扎心:我引以为豪的 P3a,其实不是「底座」,是「第一个应用」(app #1)—— 它的登录、账号、会话逻辑焊死在网关里,用的是网关自签的终端用户 JWT。也就是说, 底座对「一个真正独立的外部应用」的接入契约,至今一次都没被验证过。P3a 全程是 「网关既当底座、又当 app #1」的特例。这,才是 P4 真正的未知数。

教训沉淀 · 别把「便利」当「架构」。 我一开始想让所有产品的全部流量都经统一网关。对抗性一想才发现:这么做,网关就是一个 多租户 API 网关平台——我嘴上说「不做平台」,手上却在造平台。后来把它框成 「分阶段」:统一网关是验证阶段的便利(产品初期量小、一处好管理、按 URI 隔离), 不是永久承诺;哪个产品量起来了就拆出去独立部署。关键是——为了「以后拆得动」, 按产品的边界必须从第一天就干净(独立 URI 命名空间 + 每产品独立凭证 + 不共享每产品状态), 否则「以后拆分」会退化成「以后重写」。「统一好管理」是阶段理由,不是架构理由。

让一个 critic 来拆台

想清楚不等于想对。我把这份架构基线丢给一个独立上下文的 critic,专门让它拆台, 结果它给了 4 条 Critical + 6 条 Important,大部分我都得认:

  • 平台化矛盾:统一网关做「反代 + 应用注册 + 每应用凭证 + URI 隔离 + 跨产品观测」, 这功能面就是一个多租户平台,「只服务自有产品」是受众区别不是架构区别。→ 被我的「分阶段 + 退出路径」化解。
  • 单点:统一网关是所有产品的单点,一次坏发布 / 审核 panic / 隧道闪断 = 全部产品同时下线。 → 验证阶段知情接受,长期靠「拆分」缓解,但必须如实记进风险栏。
  • 身份可伪造:如果网关只验应用级凭证、终端身份靠应用传 header 又不校验, 那持有某应用凭证的人改个 header 就能冒充该应用的任意终端用户——而用量 / RAG 归属 / 会话 全键控在这个可伪造的键上。→ 没化解,成了下一步的头号设计题。
  • 共享 AI 层的跨产品泄漏:所有产品共用 echo/dify,租户键还不存在时,产品 A 可能检索到产品 B 的向量。 → 同上,头号设计题。

对抗验证的价值就在这:它没让我推翻方向,而是把**「先证明只共享 AI 那一环不够用,再谈全流量经网关」** 这条更省的路逼了出来,还把「身份怎么被信任」「隔离键怎么造」这两块从「隐含假设」提成了「第一等设计题」。

教训沉淀 · 对抗验证要趁「还没动手」。 这次 critic 拦下的不是一个 bug,是一个方向——如果我带着「全流量经网关」这个没论证的地基往上盖, 等第二个产品接进来才发现平台化复杂度锁死,代价就不是改文档了。架构的对抗验证越早越便宜。

顺带一个决定:P3b(飞书渠道)降级、移出关键路径。想通之后它的定位塌了—— 它是 app #1 的一个渠道,推进不了底座真正要验证的东西;剩下的价值只是「一个我自己用的飞书小工具」, 想要再随手做。补建的几份 docs/architecture/ 基线,把上面这些决策、约束、未解风险全部落成了白纸黑字。

现在站在哪,接下来去哪

已交付:P3a 四个切片(生产上线 / 审核真做 / 集成小修 / 管理端 v1)全部上线并验收通过。 更重要的收获不是代码,是把项目的定位从「一个 AI 底座」想清楚成「一层给多个独立产品共用的、分阶段的共享控制面」, 并诚实地承认:底座对独立应用的接入契约,还没被验证过。

下一站:定义第一个真实产品——它不是聊天,AI 只是它的一环。 做它接入统一网关的那一段,恰好就是那个「验证底座对应用契约」的最小实验(app-spike), 头号要答的题就是 critic 拦下的那两条:终端身份怎么被网关可信地接受、按产品隔离的键怎么造

一个人做底座,最容易犯的错不是写错代码,是把自己造的第一个东西误当成地基。 这一篇我最庆幸的,是在浇下一层水泥之前,先蹲下来看清了地基到底在哪。下一篇 (三) 见。