一位有12年Web开发经验的工程师用AI工具在3天内完成移动应用开发。这是AI编程工具提效的具体案例,展示了如何突破技术边界快速交付跨端产品。
我已经构建网络应用12年了。在这段时间里,我从未写过一行移动端代码。不是 Swift,不是 Kotlin,甚至没有写过基础的 React Native hello world。
这一切在上个月因为我的妻子而改变了。
她一直在用我为她构建的 AI 伴侣 Synapse,每天都在她的手机浏览器上使用。如果你是新来的,Synapse 是一个个人 AI,它使用时间知识图而不是简单的向量搜索来记住她生活、人际关系和情感模式的所有事情。我写过关于扩展记忆系统、将摄取移至异步、基准测试记忆框架以及为知识图构建 Notion 接口的文章。这篇文章讲的是不同的东西:移动端。
从手机浏览器上的体验可以用,但感觉不太对。她想要一个真正的应用。一个生活在她主屏幕上、能瞬间打开、不显示浏览器地址栏的东西。
我多年来一直在回避移动端开发。它总是感觉像一个完全不同的世界,有不同的工具、不同的部署、完全不同的一切。但有两件事让我重新考虑。首先,React Native 结合 Expo 已经变得非常好用。它足够接近我已经知道的网络开发,以至于跳过去不会那么可怕。其次,像 Cursor 和 Claude 这样的 AI 工具让我确信即使在我从未接触过的领地,我也能快速前进。
还有一件事让这成为可能。Synapse 是用 Turborepo 构建的 monorepo。网络应用、后端、共享包和现在的移动应用都在同一个仓库中。这意味着 AI 代码代理可以一次看到所有内容。当我让它构建一个新屏幕时,它可以查看现有的网络组件、Convex 后端、共享类型,并生成真正合适的代码。没有上下文切换,没有在仓库之间复制粘贴。它就是能工作。
所以我打开了终端,开始了。
第一次提交:2026 年 4 月 3 日。到 4 月 5 日,我有了一个包含 Clerk 身份验证、聊天 UI、实时流式传输、内存管理、角色和网络版本相同深色主题的可工作应用。一切都由相同的 Convex 后端驱动。
以下是 3 天工作的样子:
该应用有一个入职流程,解释 Synapse 的工作原理:你聊天,它摄取你的对话,将其编译成一个知识图,并随时间演变。从侧边栏,你可以访问你的会话、内存、角色和计划。聊天界面实时流式传输 AI 响应。角色屏幕让你在不同的 AI 模式之间切换,比如 Brujula(基于 ACT 和 DBT 的治疗伴侣)、Calma(通过积极心理学的情感支持)、Focus(纯技术模式,无内存上下文)以及其他。内存浏览器显示具有 45 个节点和 47 个关系的完整知识图,你可以检查任何实体并查看其连接。
Monorepo 是这里的秘密武器。用于身份验证的 Clerk、用于导航的 Expo Router、网络应用已经使用的相同 Convex 变更和查询。我没有必要重建任何后端逻辑。共享包意味着类型、API 定义和验证已经存在。我只需要构建屏幕。
三天。对于从未接触过移动端的网络开发者来说,这感觉不真实。但这个速度隐藏了一个直到我的妻子实际开始使用它时我才会发现的问题。
一切都在我的模拟器上工作。然后她在她的手机上开始使用它。
在网络上,流式传输 AI 响应很简单。你调用 fetch,从 response.body 获取 ReadableStream,并在数据到达时读取块。干净、现代、可靠。
React Native 在 iOS 上不支持这个。
为 React Native 在 iOS 上提供动力的 Hermes JavaScript 引擎不实现 fetch 响应体上的 ReadableStream。所以标准网络方法根本不起作用。解决办法是使用 XMLHttpRequest,设置 responseType: "text",然后监听 onprogress 事件。每当新数据到达时,xhr.responseText 包含目前为止接收到的所有内容。你比较长度来跟踪什么是新的。
以下是它的样子:
const xhr = new XMLHttpRequest();
xhr.open("POST", `${CONVEX_SITE_URL}/chat`);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
xhr.responseType = "text";
let lastLength = 0;
xhr.onprogress = () => {
const currentText = xhr.responseText;
if (currentText.length > lastLength) {
lastLength = currentText.length;
updateStreamedContent(currentText);
}
};
这在开发中效果很好。我可以看到 AI 响应逐字流式传输,就像在网络上一样。我发布了它。
然后我的妻子开始以人们实际使用手机的方式使用该应用。她会问 Synapse 一些东西,然后在等待响应时切换到 WhatsApp。或者她会锁定屏幕。或者她会检查几秒钟 Instagram。
当她回来时,响应消失了。空白消息。或者一个错误。
以下是发生的情况。iOS 主动挂起进入后台的应用。当她切换走时,iOS 杀死了网络请求。XMLHttpRequest 连接无声地断开了。但是 AI 后端已经开始生成。服务器继续运行,生成令牌,消耗成本。响应就是没有地方去。手机不再监听。
这不是你在开发中能抓到的 bug。你在某人以真人使用手机的方式使用应用时抓到它:他们永远不会停留在一个屏幕上。他们不断切换。每次切换都是 iOS 杀死你连接的机会。
关键的洞察很简单:停止将客户端作为流的唯一接收者。
Synapse 使用 Convex 作为其后端。移动应用发送请求到一个 Convex HTTP 端点,该端点将其转发到 AI 服务(Cortex)并将响应流回去。在修复之前,这是一个直接管道:Cortex 生成,Convex 流式传输到客户端,客户端渲染。如果客户端消失,管道断裂,之后的一切都丢失了。
修复是让服务器意识到断开连接并无论如何继续。
if (delta?.content) {
content += delta.content;
if (!clientDisconnected) {
try {
await writer.write(encoder.encode(delta.content));
} catch {
clientDisconnected = true;
console.warn(
"[http /chat] Client disconnected, continuing generation server-side",
{ requestId, contentLengthSoFar: content.length }
);
}
}
}
当 HTTP writer 尝试发送一个块并失败时(因为客户端消失了),服务器设置一个 clientDisconnected 标志,记录一个警告,并继续生成。它不会停止。它不会抛出。它只是停止尝试写入一个死连接,并继续累积响应。
在生成的末尾,无论客户端是否仍然连接,服务器进行单个数据库写入以持久化最终内容:
await ctx.runMutation(internal.messages.finalizeGeneration, {
id: assistantMessageId,
content,
metadata: { model: modelUsed, usedFallback, /* ... */ },
completedAt: Date.now(),
});
当用户重新打开应用时,消息已经在那里。完全生成。存储在 Convex 中。无需重试,无丢失令牌,无空白消息。
还有一个边界情况要处理。移动客户端有错误处理,当流失败时调用 reportStreamFailure。但如果服务器在客户端断开连接后成功完成了生成呢?客户端会回来,看到 XHR 失败,并尝试将消息标记为错误,覆盖服务器已经保存的完全好的响应。
export const reportStreamFailure = mutation({
args: {
messageId: v.id("messages"),
errorMessage: v.optional(v.string()),
},
handler: async (ctx, args) => {
const message = await ctx.db.get(args.messageId);
// Don't overwrite a message the server already finalized
if (message.completedAt !== undefined) {
console.log("[reportStreamFailure] Skipped — already finalized");
return;
}
await ctx.db.patch(args.messageId, {
type: "error",
content: errorContent,
metadata: { errorCode: "CLIENT_STREAM_FAILURE" },
completedAt: Date.now(),
});
},
});
如果消息已经有 completedAt 时间戳,失败报告将被忽略。服务器赢得了竞争。响应是安全的。
整个流程是这样的:客户端发送请求,服务器开始生成。客户端断开连接,因为 iOS 挂起了应用。服务器在下一个块写入时检测到断开连接,设置标志,并继续生成。每个块在内存中累积。当生成完成时,服务器将完整响应持久化到 Convex。客户端重新连接并找到完整的消息在等待。
经过一个月的跟踪,以下是现在的情况:
来自网络应用的 546 条消息
来自移动应用的 239 条消息
移动占所有使用量的 30.4%
趋势很清楚。在最近几天,移动使用量与网络使用量相匹配或超过。5 月 1 日,有 10 条移动消息和 9 条网络消息。她现在比浏览器更常使用手机。
没有断开连接修复,几乎三分之一的所有交互都会不可靠。每次她在响应中途切换应用(这是不断进行的),消息都会丢失。
转向移动端也带来了超出工程时间的成本。Apple Developer Program 每年 99 美元。Google Play 是一次性的 25 美元费用。绝对术语上不多,但这是你在构建一个副项目而不是资金充足的初创公司时会思考的那种事情。
设备是画布。相同的 AI、相同的后端、相同的知识图,但手机改变了她使用 Synapse 的方式。烹饪时的快速问题。在路上检查记忆。交互比网络上更短、更频繁、更自发。
为移动端构建迫使我以网络从不做过的方式思考弹性。在网络上,如果某人切换标签,你的 JavaScript 继续运行。连接保持打开。响应到达。你甚至不会想到它。在 iOS 上,没有什么是有保证的。你的应用可以在任何时刻被挂起,如果你没有为此计划,你的用户将有一个破碎的体验。
浏览器是宽容的。iOS 不是。为不宽容的平台构建使整个系统变得更好。
如果你正在构建 AI 产品并想跟随我的脚步,我写关于向真实用户发布 AI 的真实挑战。不是理论。不是演示。那些在某人每天实际使用你的应用时会破裂的东西。
LinkedIn:Juan David Gomez
如需进一步操作,你可以考虑屏蔽此人和/或举报滥用