Shopify 放弃 React Native 这件事在 Hacker News 上拿到了 1272 分。这个分数放在 HN 的语境里基本属于“炸了锅”的级别。关注跨端开发的人、关注电商底层架构的人、以及所有被 React Native 坑过的移动端团队几乎都挤进评论区吵了一轮。真正让我上心的不是“RN 行不行”这种口水仗而是 Shopify 后续暴露出来的架构转向思路把业务逻辑往服务端下沉客户端只保留最薄的 UI 层和交互层。这个思路翻译过来就是——无头架构Headless。无头这个词在 CMS 领域早就不是什么新鲜概念但放到 Agent 项目里很多人可能刚意识到自己正踩在同一个泥坑里。我过去半年一直在做 Agent 相关的项目最大的体会是绝大多数 Agent demo 都长一个样——一个聊天框背后一个 LLM 调用再挂几个工具函数。前端和“大脑”焊死在同一个进程里想换个入口就得重写一遍。这跟 Shopify 当初在 RN 上遇到的问题本质上是一个问题。所以我花了两个周末复现了一套剥离 UI 的 Agent 无头架构核心代码去掉了业务敏感信息下面把思路和实现完整记录下来。1. 先拆透 Shopify 弃 React Native 这件事1.1 HN 1272 分背后吵的是什么HN 上那个帖子标题我记得大概是关于 Shopify 放弃 React Native、转向原生技术的讨论。1272 分是什么概念HN 上能拿 500 分以上的帖子已经算高热度1000 分以上基本就是现象级话题。评论区里明显分成三拨人第一拨是“果然如此”派早就觉得 RN 作为跨端方案的桥接层Bridge在复杂 App 里是瓶颈第二拨是“省心省力”派做原生开发的终于不用整天为 RN 性能问题跟业务方扯皮第三拨是“RN 之死”派认为连 Shopify 这种头部用户都跑了跨端方案又要凉一大截。但说实话这三拨人里大部分都没聊到点子上。Shopify 放弃 React Native 不是单纯因为“RN 性能差”而是因为在它的体量下跨端框架带来的收益已经覆盖不了成本。这不是移动端技术圈的独有故事电商建站领域同样天天在做这类选择题。你去翻 Shopify、WordPress、自建站方案的对比讨论最后的结论往往也是同一个——没有银弹只有成本结构不同的取舍。具体来说RN 带给 Shopify 的痛点至少有三个层面。第一是桥接层的序列化开销JS 和原生之间每次通信都要过一层序列化当业务逻辑足够复杂时这个开销会被无限放大大 App 里常见的启动白屏、首屏加载慢、内存偏高等问题RN 开发者多少都遇到过。第二是双端一致性Android 和 iOS 的底层差异导致同一个 RN 组件经常要写两套兼容逻辑。第三是招聘和培训成本团队要在熟悉 JVM、熟悉 Swift/Objective-C、熟悉 JS 的三拨人之间来回协调。这三个痛点单独拎出来都有解决方案但叠加在一起就足以让人重新思考技术路线。1.2 React Native 的真正痛点不在渲染在分层我身边不少朋友把 Shopify 的决策理解成“RN 不行了”其实 RN 这几年一直在进化新架构里的 JSI、Fabric、TurboModule 都是冲着消灭旧桥接层问题去的。但 Shopify 的核心矛盾从来不是渲染性能而是分层。RN 给你的是一套客户端渲染方案它的隐含假设是UI 层、业务状态、数据获取、路由全部在客户端运行。在一屏一屏的页面里这个假设没问题。但 Shopify 这种量级的产品页面只是用户和业务系统之间的一个入口真正复杂的是背后商品、订单、库存、支付这些系统之间的协作。把 UI 层和业务层放在同一个技术栈里就意味着每次改业务规则都要动客户端每次动客户端都要排队等发版、审核、灰度、用户升级。这套流程在 C 端消费类 App 里能忍但在 Shopify 这种 B 端平台商家需要的是“今天配置明天生效”不可能等你把某个优化拖到下一个发版周期。所以 Shopify 的转向不是“RN 换原生”这么简单而是把大量业务逻辑从客户端剥出来通过 API 暴露客户端只负责展示和交互。这其实就是网络优先Online-First架构思路。如果你理解了这个前提你就能明白为什么“无头架构”这个词会在技术社区突然火起来——无头架构不是无脑把前端砍掉而是把前端从核心系统里解放出来让核心系统面向所有端提供服务。2. 无头架构到底“无”掉了什么2.1 一句话版本它是把“脑袋”和“身体”拆开讲个生活化的类比。传统软件架构是“前店后厂”前面一个柜台UI后面一个厨房逻辑和数据。柜台和厨房焊死在一个房子里你想把柜台换到街对面去就得连厨房一起搬。无头架构就是厨房还是那个厨房但柜台不再跟厨房绑死顾客通过菜单API点单厨房做好菜通过传菜口接口协议把菜送出来。你可以同时开线下店、线上小程序、电话订餐核心厨房完全不用改。这个“厨房”就是无头系统里的 Headless Core它只对外卖能力不关心你柜台长什么样。无头架构里最成熟的代表是 Headless CMS。早期的 WordPress 是传统模式页面模板、插件、数据库全在一个 PHP 进程里跑。后来大家发现同样一份内容要在官网、小程序、App、邮件里展示每个端都要重新实现一遍渲染逻辑实在太痛苦。于是 Headless CMS 出现内容存一份通过 API 暴露前端随便选技术栈Vue、React 甚至小程序原生都行。Agent 场景其实也完全一样。2.2 无头架构成熟的三件套状态、协议、可观测一个无头系统要真正跑起来我理解最少需要三件事到位。第一是状态管理。无头系统不能依赖某个页面实例来保存状态状态必须能独立存储和恢复。Web 端掉了线、App 切了后台、命令行断开了连接都不能影响核心业务。在 Agent 场景里这意味着会话上下文、记忆、工具调用的中间结果都必须能被持久化。第二是协议。无头系统的对外统一通过协议通信协议相当于餐厅的菜单必须稳定能描述请求、响应、事件和错误。我在复现的过程中最痛苦的就是设计协议Agent 的推理过程要不要在协议里暴露工具调用失败是返回错误还是返回特殊事件这些设计直接影响所有端能不能顺畅协作。第三是可观测性。无头之后用户不再直接看到前端背后发生了什么所以系统必须能把每一步执行过程输出成日志、事件流和指标。没有可观测性无头系统等于盲人开车。这三个方面做好无头架构才是完整的不然它只是把 UI 拆出去剩下的还是一团乱麻。3. 为什么 Agent 也需要一套无头架构3.1 你的 Agent 是不是也被聊天框绑架了现在市面上的 Agent 项目我敢说 80% 都长一个样一个聊天界面一个后端接口后端拼个 prompt调一次 LLM再把结果返回给前端。有些加了工具调用Tool Calling / Function Calling有些加了 RAG但整体还是一个“聊天框应用”。这本身没有错对 MVP 验证来说这是最高效的路径。问题在于一旦你想把 Agent 的能力接入到更多场景焊死的聊天框就露馅了。比如想让 Agent 在企微或钉钉里工作你得给 Agent 包一层适配器想让 Agent 定时跑一次日报你得写个触发器去调用那个聊天接口想同时跑多个 Agent 协作比如一个负责搜索、一个负责写作、一个负责校对每个都得单独启动一个进程然后想办法让它们互相“对话”。这些都是因为前端和大脑没有拆开。我最近看到群里讨论“Agent 开发”“Agent 框架”“多 Agent 协作”头都大了但很少有人把“Agent 的入口应该多元化”当成一个架构问题来想。大家默认 Agent 就等于聊天机器人这恰恰是当前 Agent 工程化里最大的误解。3.2 Agent 的状态管理问题比 React 难十倍如果你写过 React一定知道状态管理的重要性——useState、Context、Redux、Zustand折腾得人头晕。React 的状态管理难是因为它要在 UI 树和业务状态之间保持同步。而 Agent 的状态管理更难是因为它的状态是“活”的。很多人分不清 LLM、AI 模型和 Agent 的区别这里顺便说一句LLM 只是大脑里的神经回路Agent 是整个身体——它要有记忆、有工具、有循环机制还要处理外部世界的变化。具体来说Agent 至少需要四层状态。第一层是短期会话状态当前这轮对话聊到哪了哪些信息已确认哪些还没确认通常用消息列表就能表达。第二层是中期任务状态当前任务执行到哪一步了哪些步骤已完成哪些失败需要重试需要一个任务状态机和执行图。第三层是长期记忆Agent 对用户的画像、对领域知识的积累比如用户上次提到过“预算 5000”下次再聊采购方案时应该能想起来。第四层是工具状态已经调用过哪些外部 API结果是什么结果是否发生变化。很多 Agent 项目轻视这一层结果同一工具反复调用拿到旧的脏数据还不知道。如果你把 UI 和 Agent 核心绑在一起这四层状态全堆在前端页面或者一个进程的全局变量里会话一断就全没了。但无头化之后状态层独立存在前端断线后重新连上对话还能接着聊任务执行记录也能完整回放。3.3 无头化之后Agent 能长出哪些新形态把 Agent 的核心抽成 API 之后能做的事情会突然多出一截。我几个实际跑通的场景列一下。第一个是 Web Chat这是最基础的。前端用 Vue 或者 React 写个聊天界面通过 WebSocket 或 SSE 订阅 Agent 事件流用户体验和普通聊天一样。第二个是命令行入口直接在终端里跟 Agent 对话适合在服务器上快速排查问题或者做自动化脚本的前置交互。第三个是定时任务每天早上 9 点让 Agent 自动去读待办列表、整理当日重点、推送摘要它根本不需要一个“用户”可以是一个后台常驻服务。第四个是多 Agent 协作Agent A 通过 API 把问题抛给 Agent BB 处理完再通过 API 把结果传回来中间每一轮都有完整的事件记录。这时候你会发现多 Agent 协作不再是“让两个聊天框互相发消息”而是一套标准化的 Agent-to-Agent 协议。这些场景落到最后都离不开同一个核心把 Agent 的执行逻辑和入口解耦。这不是花架子是 Agent 从“能跑 demo”走向“能落地生产”的必经之路。4. 复现实录我给 Agent 搭的那套无头后端4.1 目录结构与技术选型我选的栈是 Python FastAPI SQLite核心推理循环没有用 LangChain而是手写了一个精简版本。原因很简单LangChain 抽象层级太多出了问题不好排查而且我做的无头化核心是理清状态和协议手写能完全掌控每个环节。选 SQLite 是因为它零配置、单文件适合复现和演示生产环境可以平滑切到 PostgreSQL。目录结构我尽量精简了agent-headless/ ├── core/ │ ├── agent.py # Agent 推理循环 │ ├── state.py # 会话状态与快照 │ ├── memory.py # 三层记忆短期/中期/长期 │ └── tools.py # 工具注册与执行 ├── api/ │ ├── server.py # FastAPI 入口 │ ├── routes.py # REST 路由 │ └── events.py # SSE 事件流 ├── store/ │ ├── db.py # SQLite 存取 │ └── models.py # 数据模型 ├── clients/ │ ├── web_chat.py # Web 聊天前端示例 │ ├── cli.py # 命令行入口 │ └── scheduler.py # 定时任务入口 └── tests/ └── test_agent.py这个结构的关键在于core 层完全不知道外面是什么入口在调用它。Web、CLI、定时任务都只是 client它们做的事情完全一样——调用 API订阅事件流。如果你去看 Shopify 后来的客户端演化会发现他们也在往这个方向收敛只不过客户端侧的生态更成熟API 化做得更早。4.2 核心实现一Agent 推理循环与状态落盘推理循环是整个系统的发动机。我把它拆成四步接收输入、构造上下文、执行工具、持久化状态。第一步是接收输入。这里有个细节Agent 的输入不一定是纯文本可能是一个结构化指令、一段需要重点关注的上下文或者一个要执行的命令。我在协议里统一规定输入是一个 message 对象包含 role、content、meta 三个字段meta 里可以带附加信息比如允许调用的工具白名单。第二步是构造上下文。这一步我不会把完整历史全部塞给 LLM而是做一次“上下文化”短期记忆取当前会话最近 20 条消息中期记忆从向量库检索和当前输入相关的 5 条记录长期记忆取用户画像中稳定不变的几条。合在一起拼成一个 system prompt。第三步是执行工具。我定义了工具协议每个工具有 name、description、input_schema、handler 四个属性。LLM 返回工具调用请求时Agent 先校验参数再执行 handler把结果作为 tool message 回填给 LLM。这里我加了一个循环上限默认跑 5 轮防止 Agent 陷入无限调用。第四步是持久化状态。每完成一轮推理保存完整的状态快照包括消息列表、当前任务进度、记忆更新记录。状态快照的 id 就是对外的 run_id任何一个客户端都可以用 run_id 恢复整个执行过程。核心代码如下# core/agent.py class Agent: def __init__(self, state_store, memory_store, tool_registry, llm_client): self.state_store state_store self.memory_store memory_store self.tool_registry tool_registry self.llm_client llm_client def run(self, conversation_id, user_message): state self.state_store.load(conversation_id) state.messages.append({role: user, content: user_message.text, meta: user_message.meta}) # 上下文构造短期窗口 记忆检索 系统提示 system_prompt self._build_system_prompt(conversation_id, user_message.text) messages [{role: system, content: system_prompt}] state.messages[-20:] for _ in range(5): # 工具调用轮数上限 response self.llm_client.chat( messagesmessages, toolsself.tool_registry.schemas() ) messages.append({role: assistant, content: response.content, tool_calls: response.tool_calls}) if not response.tool_calls: break for call in response.tool_calls: result self.tool_registry.execute(call.name, call.arguments) messages.append({ role: tool, tool_call_id: call.id, content: result }) state.tool_logs.append({ tool: call.name, input: call.arguments, output: result, ts: now() }) state.messages.append({role: assistant, content: messages[-1][content]}) self.state_store.save(conversation_id, state) return state这段代码没有做复杂的异步处理核心是为了让你看清楚无头化最重要的东西状态驱动。整个循环不依赖任何前端组件输入来自 API输出也是数据UI 只是这堆数据的一种观感化呈现。4.3 核心实现二记忆分层与检索记忆这块我参考了社区里讨论得比较多的分段思路短期记忆就是会话窗口中期记忆靠向量检索长期记忆是结构化的用户事实库。短期记忆最简单就是当前会话最近的一部分消息我用上面的state.messages[-20:]实现。中期记忆我用了一个轻量的向量检索方案SQLite 加一个非常小的 embedding 接口。每轮对话结束后把这条消息的摘要文本向量化写入mem_entries表里面存了 text、embedding、conversation_id、timestamp 四个字段。检索时把当前输入向量化做余弦相似度排序取 top_k。长期记忆我用了“事实抽取”的思路。每次 Agent 拿到用户的明确偏好、时间约束、数字信息这类稳定事实时我会额外跑一次 LLM 抽取把抽取结果存为结构化的键值对。下次系统提示词拼接时这些键值对会被渲染成“已知事实”块。三个层级的查询优先级是短期 中期 长期短期能回答的不查向量库向量库能回答的不再去动用户事实库。这样可以明显减少 LLM 调用和检索耗时。4.4 核心实现三多端接入与事件流无头架构最关键的一步是事件流设计。我最终选了 SSEServer-Sent Events作为事件流协议主要原因是它对服务端实现简单长连接控制容易前端原生支持也很好。我定义的事件类型大致如下agent.message.created # 用户/助手消息创建 agent.tool.started # 工具调用开始 agent.tool.finished # 工具调用结束 agent.memory.updated # 记忆更新 agent.state.snapshotted # 状态快照生成 agent.run.finished # 一轮推理完成前端订阅事件流后可以实时渲染“Agent 正在调用哪个工具”“搜到了什么”“为什么停止”这就是无头架构下的可观测性入口。同一个事件流Web 端可以用来做流式打字机效果CLI 端可以用来做进度条调度器可以用来判断任务成功还是失败。REST API 则暴露了核心操作POST /v1/agents/{agent_id}/conversations # 创建会话 POST /v1/agents/{agent_id}/conversations/{cid}/messages # 发送消息 GET /v1/agents/{agent_id}/conversations/{cid}/history # 查看历史 GET /v1/agents/{agent_id}/conversations/{cid}/events # SSE 事件流 GET /v1/agents/{agent_id}/conversations/{cid}/state # 查看状态快照有个很值得注意的细节定时任务入口根本不需要维护会话。它每次都创建一个新会话发一条“请总结今天待办并输出日报”的指令然后订阅事件流等 run.finished 事件。成功就保存结果失败就触发告警。它和聊天框用的是同一套 API区别仅仅在入口。另外关于安全多说一句无头架构的 API 暴露面比单体聊天应用大得多工具注册时必须声明权限范围比如工具是只读还是可写、哪些角色可以用API 层要有鉴权事件流要有订阅授权。这一块我在第一版里偷懒了同事提醒之后才补上。无头化带来的便利越大对访问控制的要求就越严格。5. 实战中的坑我踩过的 5 个问题5.1 问题复盘并发覆盖、token 失控、工具结果不可复现第一个坑是并发会话状态覆盖。最开始我把状态放在进程内字典里Key 是 conversation_id结果两个客户端同时给同一个会话发消息最后一个写回的状态把前面的覆盖了。后来我改成“版本号 乐观锁”每次写回前对比版本号冲突就重试。这个问题的本质和数据库并发控制一模一样但在 Agent 系统里更容易被忽略因为聊天看起来像串行实际上完全可能并发。第二个坑是 token 消耗失控。刚开始我贪心系统提示词里塞了非常详尽的背景说明、工具列表、用户画像每个会话的 token 开销比实际对话内容还大。后来做了三层优化工具列表只按当前场景加载用户画像只取和当前主题相关的字段历史记录按相关度截断而不是全量保留。一轮对话的 token 开销从平均 6000 降到了 2500 左右。第三个坑是工具结果不可复现。之前有个查询库存的工具同一个商品Agent 查了两次结果不同。一开始我以为是工具写错了后来发现是库存数据本身在变动Agent 却把第一次的结果当成真实情况继续推导。解决办法是给工具结果打上时间戳并且在上下文里明确提示“工具结果是某个时刻的快照可能与当前状态不一致”。第四个坑是记忆污染。向量检索的 top_k 里经常混入一些没用的闲聊内容Agent 反而被带偏。后来我在写入中期记忆之前先做一道质量过滤只有包含实体、数字、结论性描述的内容才允许写入闲聊一概不存。第五个坑是无头模式下的调试困难。以前聊天框模式直接看页面就知道 Agent 在干嘛。无头化之后Agent 变成了 API出错时只能看到一堆 JSON。我后来做了一个调试界面本质就是订阅事件流再格式化展示相当于给无头系统补了一层“临时头”。事实证明这个调试界面比正式前端还重要。5.2 常见问题速查表我整理了一张问题速查表方便你复现时快速定位。现象可能原因解决方案并发会话相互覆盖状态没有版本控制加乐观锁或按会话分库token 消耗远高于预期系统提示词塞了太多无关内容按场景加载工具列表和记忆内容Agent 反复调用同一工具没记录工具调用日志在状态中加入 tool_logs 用于去重对话到一半前端断了恢复后失忆状态没有持久化快照每轮存档提供 run_id 恢复检索内容把 Agent 带偏记忆库写入太随意写入前做实体和结论过滤事件流断连重试风暴SSE 没有断点续传机制事件加序号客户端记录 last_event_id其中最后一条我想多聊两句。SSE 断连是常事如果不记录 last_event_id重连后会把已消费的事件再消费一遍。我后来给每个事件加了一个自增序号客户端拿着 last_event_id 回来服务端直接从下一条开始推。这个语义很像消息队列里的消费者位移搞过 Kafka 的同学应该秒懂。6. 这套架构的边界和它留下的思考6.1 它和 LangChain、CrewAI 这些框架是什么关系这个话题是我在复现过程中被朋友们问得最多的。我的回答是LangChain、LangGraph、CrewAI 解决的是“Agent 内部怎么写”的问题我做的这套无头架构解决的是“Agent 怎么对外服务”的问题两者不是替代关系而是不同层次。如果非要比LangGraph 的 StateGraph 概念和无头架构里的状态快照很像但它主要面向图编排不解决多端接入和事件导出。CrewAI 解决多 Agent 的角色分工和任务委派但它的 Agent 之间通信默认是进程内消息一旦要跨进程、跨语言还是得自己设计协议。我建议的做法是内部编排可以继续用你熟悉的框架对外暴露的一定要是一套稳定、协议化的 API。框架可以换API 不要随便换。这样即使未来从 LangGraph 切到自研编排所有客户端都不受影响。6.2 什么情况不要无脑上无头最后泼一盆冷水。如果你的 Agent 只是一个两三周验证的 demo我强烈建议不要上无头架构直接聊天框加进程内变量就够了。无头架构的成本在协议设计、状态持久化、事件流、可观测性这几个方面前期投入很大收益要等到真正多端接入时才会显现。什么情况下值得上当你确认至少有两个入口要接入同一个 Agent 核心或者你需要对 Agent 执行过程做完整的审计和回放或者你需要把 Agent 作为服务提供给其他内部系统调用这时候无头化就是必然趋势越早做越省钱。我自己判断的临界点很简单当“开始考虑这个 Agent 在聊天框之外还能怎么用”这个问题出现时就是该无头化的时候了。我复现这套东西不是为了论证 Shopify 的决定绝对正确也不是为了让大家丢掉手头的框架重新造轮子。我自己最大的感受是Agent 工程化现在最大的瓶颈不是单个模型的能力而是产品形态和系统边界。过去一年市面上出现了大量 Agent 框架、Agent 记忆方案、多 Agent 协作的讨论但真正到了生产环境被问住的往往是“这个能力如何稳定地暴露给多个系统用”“这条执行链路怎么审计”“智能体断线之后怎么恢复”。这些问题和 Shopify 在移动端遇到的问题同构无头架构给的也是同一种答案把核心逻辑和表现层解耦用协议和状态来约束边界。最后分享一个复现时的小技巧不要一上来就写完整协议先把最核心的“发送消息—接收事件”跑通再加状态快照再加工具调用。每次只加一个环节问题好定位很多。这套代码我已经整理过去掉业务敏感信息后结构还算干净感兴趣的话照着目录结构从 core 层开始读关键处都写了人话版注释。
