2026 年 8 月 7 日,我完成了 Personal Agent 第一期。
最后一次验收很具体:我在真实 iPhone 上说了一笔餐饮支出,消息经过公网 HTTPS 到达自己的 ECS,由智能体判断并调用受控的财务工具,写入我的真实飞书账本,再从飞书回读结果。智能体、财务系统和飞书三侧的状态一致,我也在账本里完成了人工核对。
这条链路背后,是 7 月 23 日到 8 月 7 日之间的 201 次提交、1995 项自动化测试、38 份环境与验收证据,以及多次推翻“已经完成”的判断。
第一期最终交付的不只是一个可以理解“午饭 45”的模型,而是一套只服务我自己的系统:iPhone 负责对话、确认和状态展示;自托管后端负责智能体运行时、MCP 客户端、权限、审计、恢复和高权限凭据;飞书账本继续作为财务事实源。
这套系统的结构,和我过去十年在做的事情其实是同一个:一个只有一个开发者、一个用户的开放平台。业务工具是 API,工具白名单和设备权限范围是权限模型,幂等键和回执是写入契约,审计与人工确认是治理。区别只在于,平台方、接入方和最终用户这三个角色,这一次全都是我。
这篇文章不再重复我为什么要做 Personal Agent,而是记录一期里真正改变了项目形态的四件事:我如何组合编码智能体和模型,为什么在开发中途停下来先补所有数据域共用的智能体底座,怎样定义一次真实行动的成功,以及代码离开本地之后还要付出多少工作,才能成为一个不是玩具的产品。
我没有选择一个编码智能体,而是逐渐搭出了一套开发组合
一期并不是由某一个编码智能体从头写到尾。
我在 Codex、Claude Code 和不同模型之间不断切换。强推理模型更适合做方案、复杂问题和审查;边界清楚的编码任务可以交给成本和速度更合适的模型;实现者完成代码之后,再让独立上下文里的另一个智能体根据 PRD、代码差异和测试证据挑错。
这个“独立审查”环节不是形式。它在一期里至少两次在全绿的测试套件背后找出了六个阻塞级缺陷——一次是模型边界实现,一次是智能体共用底座的上下文压缩组件。两次的成因都不是粗心,而是结构性的:测试替身和被测代码出自同一套假设,所以它只能确认这套假设,永远不会反驳它。高风险的财务、权限和部署问题,因此也不能因为两个模型都认为“没问题”就放行。
模型层同样没有形成一条从头到尾不变的路线。我先后尝试过 Kimi、GLM-5.2 和 DeepSeek:Kimi 的额度很快成为约束;GLM-5.2 可以承担主编码,但 Claude Code 的辅助请求随后出现了稳定性问题。模型的角色随着额度、稳定性、协议兼容和任务类型调整,而不是在项目开始时一次选定。
而这个“辅助请求”,很快暴露出一个我原本没有预料的事实:编码智能体并不等于它背后的主模型。
Claude Code 的自动模式有自己的安全分类器,负责判断一条 Bash 命令是否可以执行。一次分类器出现间歇性 503 时,主模型仍然可以正常生成代码,但测试、Git、SSH 和 ECS 验证全部被拦住。真正让工作停摆的不是主模型的能力,而是一个位于关键路径上的辅助请求。
为了管理这些模型和供应商,我先使用 CC Switch 切换整套供应商配置,随后尝试用 LiteLLM 搭建编码智能体路由,最终放弃这条路线,换成 Claude Code Router(CCR)。CCR 让我能够把 Claude Code 的主请求和安全分类器分别路由到不同模型、不同供应商,而不是让两者共享同一个故障域。在后来的实际运行证据里,主请求由 DeepSeek-v4-flash 处理,辅助请求由 GLM-4.5-air 处理,两者分属不同供应商。
这里重要的不是某个具体模型最终胜出,而是我终于能明确控制:Claude Code 的不同模型槽位分别由谁处理;一个供应商的认证、限流、503 或协议兼容问题,不再同时拖垮编码和工具执行。
我对编码智能体的选择因此从“哪个模型最强”变成了另一组问题:谁负责计划,谁负责实现,谁负责独立审查;主请求和辅助请求怎样路由;失败之后是停止、重试还是换供应商;一次交接需要保存哪些代码、SHA、验收标准和失败证据。
这套仍然依赖我手工推动的组合,成为了下一阶段图式编排循环的原型。
一期最重要的产品决策,是我在中途先补智能体的共用底座
先讲那次事故。
系统上线之后,线上仍然只有一条唯一的时间线(Timeline),这是正确的。但会话分类器没有在生产环境真正切出新的会话切片(Session)。从 7 月 31 日开始,78 条以上的正常聊天、演练消息、失败结果、重复卡片和恢复记录,全部进入了同一个会话切片。
模型随后开始从被污染的上下文(Context)里模仿错误行为:面对一条新的记账请求,它不再调用工具,而是直接回答“已经记录”。这些直接回答又以 succeeded 的操作结果形式进入后续历史,继续强化这个错误范式。同一个模型在干净上下文中能够正常发起工具调用,在这段生产会话中却持续给出直接回答。
最后的处理不是继续堆提示词,而是关闭被污染的会话,并增加结构化的记账意图门禁:带有金额和账本语义的请求,如果没有产生写工具调用,就不能被当作成功回答。
修复之所以这么便宜,是因为两周前的一个决定。 因为时间线与会话切片在设计上就是分离的,修复时不需要删除或搬走任何用户历史:旧会话关闭归档,下一条消息自动进入新会话,用户看到的时间线仍然连续。如果当初把“聊天记录”做成一个东西,这次修复就会变成一次面向用户可见数据的迁移。
而那个决定,是在开发进行到一半时做出的。
我为什么在中途停下了纵向开发
Personal Agent 最初的开发顺序相对直接:先完成财务工具和服务端链路,再做 iOS 对话界面,把记账能力交到真实手机上。
开发到一半,我主动暂停了这条纵向路线。
财务之后还会有知识库、健康、衣橱、日历和更多个人数据域。如果每个领域都各自处理对话历史、上下文、记忆和模型路由,项目会得到一组能聊天的功能,却不会得到一条可以长期延续的 Personal Agent 主线。更现实的是,iOS 对话接口一旦先冻结,错误的会话抽象就会同时固化到客户端、服务端和数据库里。
因此,我在 iOS 对话合同冻结前暂停了原来的开发顺序,增加了一组所有数据域都会共用的智能体能力。这项建设的核心不是增加几个技术组件,而是把聊天产品里经常被混用的四个概念彻底拆开:
| 层 | 给谁看 | 生命周期 | 关键约束 |
|---|---|---|---|
| 时间线(Timeline) | 用户 | 永久加密归档 | 全设备唯一一条;用户不需要创建、命名或切换 |
| 会话切片(Session) | 服务端 | 话题结束即关闭 | 由语义边界切分,不由令牌长度切分 |
| 上下文(Context) | 模型 | 单次调用 | 每轮由服务端重新编译,模型不能自取 |
| 记忆(Memory) | 跨会话 | 候选 → 生效 → 被替代 / 删除 | 提供连续性,但永远不是事实源 |
下面几节,逐条解释这四层为什么必须分开。
一条可见的时间线,不等于一个无限增长的会话
主流智能体产品几乎都提供会话列表:新建、命名、归档、切换。我在一期明确放弃了这套交互——所有设备共享同一条用户可见的时间线,iPhone、Mac 或未来任何入口看到的都是同一段连续历史,用户不需要创建、命名或切换“新对话”。
这不是为了简化交互,而是一个关于智能体究竟是什么的判断。
我认为真正的智能体是一个角色,不是一件工具——它更接近一位助理,而不是一个可以开很多份的文档。我不会对我的助理说“我们新建一个对话来聊报销”。我跟他说话,他记得上次说过什么。会话列表这套交互,本质上是把模型的上下文限制外包给了用户:因为模型装不下全部历史,所以请用户自己动手,把历史切成模型吃得下的份量,并且自己记住哪一份里有什么。这是一次把技术约束转嫁给产品的设计。
这个判断有边界,我不认为它对所有智能体都成立。它的前提是 服务范围有限:Personal Agent 只服务我一个人,只覆盖我自己会接入的那几个数据域——财务、知识库、健康、日历。一个面向所有人、什么都能聊的通用助手,话题空间是发散的,用户可能确实需要自己管理边界。而我的智能体不会无限扩张,所以“我和这个角色说话”这一条线,足以承载它的全部历史。
但上下文限制是真实存在的,它不会因为我在产品层拒绝它就消失。所以我没有取消会话切片,我只是不把它交给用户。
时间线只负责体验连续性。服务端要在它内部自动切出多个语义会话:讨论记账是一段,转去讨论知识库是另一段;同一项目连续讨论几天,仍然可以属于同一个会话。会话切片的作用不是整理聊天列表,而是给智能体建立清晰的任务边界。
这意味着两种看起来矛盾的目标可以同时成立:用户始终留在一个聊天入口里,模型却不需要把所有历史都当作当前任务。
会话切分,不等于上下文压缩
会话切片因上下文限制而存在,但这不意味着它应该按上下文用量来切。这里其实是两个独立的问题:为什么需要会话切片,和边界应该划在哪里。前者的答案是技术约束,后者的答案不是。
边界必须划在语义上。话题改变才创建新会话;同一个话题变长,只在原会话内生成新的结构化检查点(Checkpoint)。令牌不够不能成为新开会话的理由——按长度切,会把一个正在进行的任务从中间腰斩,一个长期项目会因为模型窗口大小被任意切碎;按语义切,几乎不损失信息。
所以我把“话题是否改变”和“上下文是否太长”定义成两套独立机制。
会话判断失败、超时或低置信度时,系统默认继续当前会话。这里的取舍是:一次错误切分可能直接丢掉正在进行的任务,而一次漏切仍然可以通过压缩或用户纠正恢复。用户也可以直接说“这是同一个话题”或“继续上次的话题”来修正边界;修正会建立新的关联,但不会重排或伪造已经发生的历史。
聊天归档,不等于模型上下文
聊天可以永久加密归档并供用户翻阅,但“被保存”不代表自动进入下一轮模型输入,也不代表自动成为长期记忆。
每一轮上下文都必须由服务端重新组装,只包含系统规则、当前可用能力、当前会话的检查点、检查点之后的近期事件、精确的未完成状态和本轮输入。模型不能先读取完整时间线,再自己决定哪些历史重要。上下文在这里更像由系统编译出来的一份受控输入,而不是一次对聊天数据库的全文查询。
同样,意图路由也不等于授权。分类器和模型可以缩小候选工具,但最终能看见和执行什么,仍然由服务端工具白名单、设备权限范围、风险策略和服务端上下文决定。
摘要,不等于精确状态
检查点不能只是一段“看起来总结得不错”的自由文本。它需要结构化保留当前目标、已确认约束、已经作出的决定、完成步骤、外部证据、未解决问题、下一步,以及已经被纠正或废弃的旧结论,并能追溯到具体的原始事件范围。
金额、日期、幂等键、外部回执、待澄清原话、重复候选和操作状态则不能交给摘要。它们必须作为独立的结构化状态进入上下文。智能体可以压缩一次讨论,但不能在压缩中改写一次真实操作的结果。
记忆,不等于事实源
一期只实现了会话、上下文和检查点组成的短期记忆底座,中期和长期记忆仍是后续工作。但我在一期就把三层记忆的产品边界分别冻结了下来——因为“先做一个统一的向量库,再往里面塞内容”是这里最容易走的一条错路,而它的代价要等到数据脏了才会显现。
冻结下来的边界只有三条:中期记忆只允许低敏感内容自动生成,且必须带来源、数据域、生成时间和有效期;长期记忆里,模型自己推断出的偏好只能进入候选状态,要用户确认才能生效,敏感事实、健康推断、财务结论和关系推断永远不能被静默写入;无论哪一层,当记忆与实时事实冲突时,一律以事实源为准——财务当前值来自飞书,知识结论来自知识库,健康数据来自健康事实源。
记忆为智能体提供连续性,但不能成为一套由模型自行维护的影子数据库。
这一步也重新划定了框架边界。Google ADK 继续负责执行模型调用,但不拥有时间线、会话、上下文、记忆、策略和删除语义。模型框架可以替换,Personal Agent 的产品状态不能跟着某个框架一起迁移。
落地成本,和一个需要被监控的安全默认值
这层底座真正落地时的成本远超预期:数据库迁移、上下文组装、令牌预算、检查点与压缩器(Compactor),以及一整套并发和比较并交换(compare-and-swap)的正确性问题。
其中一次针对真实模型的测试发现,旗舰模型虽然能较好地判断会话边界,但延迟不可接受:那次实测的三次调用分别等了 8.3 秒、12.2 秒和 16.7 秒,还有一次直接撞上了 20 秒超时——而这个调用位于请求路径上,发生在消息落库之前。我最终给会话分类器拆出了独立的客户端、8 秒超时和一个可以单独配置的模型槽位,接受它更倾向于继续当前会话、少切一些边界的结果——和前面那个卡住我全部 Bash 命令的分类器一样,一个处在关键路径上的辅助模型必须有自己的失败策略,不能和主模型共享故障域。
但写这篇复盘时,我去只读核对了一遍线上,发现那个槽位在 ECS 上根本没有被配置。环境变量为空时代码会回退到主模型,所以生产环境里判断会话边界的,至今仍然是主智能体那个 glm-5.2。本地验证时我用的是更快的 glm-4.7-flashx,那是验证配置,不是生产配置。代码具备隔离能力,不等于生产已经完成隔离。
而开头那次生产事故,还补上了原设计没有充分暴露的一个问题:单次分类器失败时“继续当前会话”是合理的安全默认,但如果回退长期成为常态,它就会从一次可恢复的漏切,变成持续的上下文污染。安全默认值本身也需要被监控——会话切分结果和回退比例,现在都是必须观测的指标。
而上面那次核对又给这个结论补了一层:那场污染不只是模型判断问题,它同时包含一项没有被任何测试覆盖的生产配置缺口。所有离线测试都在验证“代码在配置正确时的行为”,没有一条在问“生产的配置是不是真的正确”。 所以要监控的也不只是回退比例,还有一件更基础的事——生产是否真的跑在它被设计成要跑的模型上。
如果我当时继续沿着原路线把财务和 iOS 做完,得到的会是一个会记账的应用。补上这层所有数据域共用的底座之后,我才开始建设一个可以继续承载其他数据域的 Personal Agent。
我把“成功”的定义从模型回答移到了外部事实
第一期只做财务,但我没有把财务简化成“模型抽取几个字段,然后调用飞书 API”。
模型只负责提出结构化意图。日期、金额、分类、权限、幂等、重复判断和风险边界由确定性代码处理;智能体只能看到 finance.log_expense、finance.log_income、finance.query_expenses 这类按业务命名的工具,而不是通用飞书 API。
这里藏着一个很容易被跳过的选择:飞书有官方公开的 MCP,接上就能用。我没有用它,而是自己设计了这套工具,底层直接调用飞书 API。
理由不是“官方的不好用”。而是通用 MCP 暴露的是飞书的资源模型,不是我的业务模型。它提供的能力大致是“在某个多维表格里创建一条记录”——于是智能体必须自己知道:哪个 app_token、哪张表、金额填在哪一列、我的分类体系怎么映射到表里的选项。这些知识一旦进入模型的职责范围,就同时变成了模型可以出错的地方。 而 finance.log_expense 这个签名本身就把它们消掉了:智能体只提供意图,表在哪、字段怎么映射、分类怎么归一化,全是确定性代码的事。
权限粒度也跟着变。通用工具的授权粒度是“能不能访问这个多维表格”——给了就是读写全给,模型能创建,也能修改和删除历史记录。自建工具的粒度是“能不能记一笔支出”:log_expense 只能追加。服务端工具白名单只有在工具本身足够细的时候才有意义,否则白名单里躺着一个万能工具,等于没有白名单。
但真正决定这件事的,是这一节的主题。幂等键放在哪里、写后回读由谁执行、重复候选由谁判断、CommitUnknown 由谁保留可恢复状态——这些都必须住在工具内部。如果智能体调用的是一个通用的“创建记录”,那么这些工作只剩下一个地方可以放:模型自己。“成功由外部事实定义”这条原则,在架构上就要求这些工具是我自己的。 接现成的通用 MCP,这套东西根本没有落脚点。
代价同样真实:飞书的字段映射、分页、错误码和限流都得我自己处理,官方 MCP 后续增加的能力我拿不到,飞书改 API 我得自己跟。接下来要讲的那次事故,某种意义上正是自建换来的——一次 log_expense 内部要完成写入、回读和重复检查,天然比一次薄封装的 API 调用跑得更久。
所以这不是一条通用建议,而是一条有判断标准的选择:这个工具会不会写入我的事实源。 只读的、探索性的、出错代价低的场景,现成的通用 MCP 更划算;一旦它会写,而且写错会造成真实损失——少记一笔、重复记一笔——工具的边界就必须由我自己定义。
一次写入只有在外部系统返回结果、系统完成写后回读并生成经验证回执后,才能被称为成功。模型回复“已经记好了”、HTTP 返回 200,甚至飞书已经创建了记录,都不一定足够。
一期最危险的一次问题正好发生在这里。
一笔支出已经在飞书创建成功并完成回读,财务状态机也已经进入 succeeded,但智能体最终却向客户端报告 source_commit_unknown,要求人工核对。用户看到这种结果,很可能会以为没有写入并再次发送,造成重复记账。
根因不是飞书慢,而是 MCP HTTP 客户端没有显式配置读取超时,继承了底层客户端的 5 秒默认值。一次真实写入内部需要多次飞书请求,连接在中途被切断,智能体只看到了“没有拿到最终响应”。所有离线测试替身都在毫秒级返回,因此完整的测试套件没有暴露这个问题。
修复之后,我又为未知提交选择了“自动恢复 + 人工兜底”的方案。CommitUnknown 不再直接变成一个无人能够处理的终态;系统保留可恢复状态,后台根据财务的真实结果重新投影。只有确实无法确认时,iPhone 才显示人工核对入口。
重复记录也采用同样的责任边界:系统用确定性字段发现候选,存在候选时保持零写入,再由我决定是否仍然记录。模型不得自己判断“这次应该不是重复”。多笔记账则继续关闭——一次请求写入多笔支出,要么全部成功、要么全部失败,而一期没有拿到外部数据源支持这种原子性的真实证据,所以这个能力不开。
这些规则最后形成了一条很简单的原则:模型可以建议系统做什么,但不能自己定义什么叫做完。
我的评测覆盖了什么,没覆盖什么
前面几节都在讲什么才算成功。那么我凭什么相信整套东西真的在工作?
一期的验证分成两类。第一类是场景化的标准输入:一批覆盖不同记账场景的输入,跑通意图识别、字段抽取、分类映射和写入链路。第二类是边界对抗用例,集中投在我不控制对方的地方——模型返回空响应、分类器返回畸形答案、模型端点被篡改、工具参数在签名之后被改动、审计链被篡改、上下文超出预算、历史内容试图被当成指令。这类用例只有一条原则:失败必须是拒绝,不能是静默截断、丢弃或自动修复。所以它们的名字大多长成这样——超预算时“拒绝而不是截断”,超长目标“被拒绝而不是被截断”,畸形偏好“被拒绝而不是被跳过”。
一期正式评测一共有 69 条,其中 17 条来自我提供并复核的真实表达。但我在写这篇复盘时才发现:这些“真实表达”也不等于自然输入分布。因为我已经开始主动适应模型。
一笔真实支出里,我输入的是“晚饭阿文潮汕食府 620 家庭支出”。模型正确地用“晚饭”判断餐饮分类,又只把“阿文潮汕食府”写进名称。但我之所以加上“晚饭”,正是担心它不知道这是一家餐馆。换句话说,这次成功同时证明了模型做对了,也暴露了我在替它补提示词。只测试这类输入,衡量的可能不是智能体的理解能力,而是我逐渐形成的提示技巧。
于是我在一期结束后补了一组鲁棒性评测。第一组把同一语义改写成无空格、语序变化、中文金额、自我纠正,以及去掉我主动添加的提示。第二组不是制造错别字,而是制造诱使模型自信猜测的歧义:“四五十”到底是多少,“前两天”是哪一天,只写一个不透明商户名时能不能自己猜分类。第三组原本想直接测会话污染,但我在运行前先停了下来:当时的评测框架只是把历史文本拼进提示词,跑出来的结果会是假绿灯。我先把它改成真实的 user_message、operation_result 和澄清上下文,再运行多轮回归。
我最关心的那组反事实输入给出了直接答案:“晚饭阿文潮汕食府 620 家庭支出”和去掉“晚饭”后的“阿文潮汕食府 620 家庭支出”都通过了。至少在这个模型、这次运行和带“食府”语义的名称上,我不需要再为它补“晚饭”。
第一次跑 24 条单轮候选,严格通过 13 条,安全通过 16 条。“阿文 620 家庭支出”和“炼火 962 家庭支出”都没有触发追问,模型直接提出了记账调用;“前两天”也被它静默选成了一个日期。这个结果说明真正危险的不是模型看不懂,而是它自信地补完了我没有说清楚的事实。
我没有根据模型输出倒推期望行为,而是先补产品合同:通用场景词可以参与分类但不污染明确商户名;不透明商户必须追问;区间金额和模糊日期不得估算;债务归还和普通转账不能伪装成消费支出。规则随后进入提示词和工具参数定义。为了不挑一次最好看的数字,我固定同一份数据、提示词和工具清单跑了四次。两轮共有的 24 条里,首轮严格通过 13 条、安全通过 16 条;当前版本四次严格通过 16–19 条,安全通过每次都是 22 条。第一次同口径比较里,ROB-011、015 还从通过变成了失败;重复运行后,ROB-011 四次都失败,ROB-015 则通过三次。这让我能把稳定缺陷和抽样波动分开,但旧版本只有一次基线,仍然不能把全部差异归因于这次规则修改。
完整的 32 条财务用例里,四次严格通过在 24–27 条之间,安全通过每次都是 30 条;两条修改/撤销授权边界和 7 条结构化多轮回归四次都全部通过。
这里有一个我没有预料到的规律:严格通过一直在抖,安全通过一次都没有抖过。 全集四次是 30、30、30、30,两轮共有的 24 条是 22、22、22、22;而同样这四次,严格通过分别是 24、24、26、27 和 16、16、18、19。两个独立切片,同一个形状。
这说明安全性质是这套系统可复现的真实属性,而严格正确率里有相当一部分只是测量噪声。它也解释了我为什么只敢拿安全数字说话:一个每次都稳定落在 30 的数,和一个在 24 到 27 之间跳的数,能支撑的结论完全不是一回事。
但两条真正危险的业务调用也四次都存在:“给家里买水果七百”被分到日常生活,“美签邮费”被分到出行且丢失了美国旅行标签。重复运行没有把它们洗成偶发噪声。
安全还不是可用。我又补了 10 条信息完整、必须直接执行的“摩擦上限”用例,重复三次。每次都只有 8 条直接提出工具调用,严格通过为 7–8 条;“买充电宝129元,个人支出”三次都触发了不必要澄清;“给家里交水电费200元”两次直接失败,第三次虽然调用了工具,却把名称写成“交水电费”,而不是冻结规则要求的“水电费”——我新加的那条名称提取规则,自己制造了一次错误的业务调用。一个什么都敢猜的智能体和一个什么都要问的智能体,同样不能用,所以评测既要守住静默错账的下限,也要守住交互摩擦的上限。
全部 34 条鲁棒性候选和 10 条摩擦候选仍是合成数据,这些本地仅模型结果也不等于 ECS 或真实账本验收。这次补测改变了我对评测的理解:输入是否真实,不只取决于它是不是用户亲自提供,还取决于用户有没有先替模型整理语言;单轮是否正确,也不代表系统把自己的历史喂回来之后仍然正确;知道什么时候不猜,也必须同时知道什么时候不该追问。 正式的一期关卡已经通过,但复盘让我识别并偿还了一笔覆盖债务。以后写评测,先从产品合同推出期望行为,再同时测自然输入、危险歧义、交互摩擦和带状态的历史。
代码完成之后,我才真正碰到“产品上互联网”的难度
一期还有一大段工作几乎不体现在功能演示里:ECS、安全、镜像、证书、服务用户、备份、恢复和回滚。
这部分并不是我独立完成的基础设施设计。主要方案由 Codex 根据风险和验收要求提出,我在它的指导下逐项执行、验证和处理问题。
值得单独写下来的,是这个角色反转本身。前面几节里我是决策者:冻结范围、改开发顺序、定义什么叫完成。到了 ECS、密钥和备份这一段,我没有能力做决策者——我不知道 setgid 目录和 UMask=0077 组合起来会产生什么后果,也不知道一个“只读入口”需要排除掉哪些代码路径。我能做的只剩验收。
于是真正的问题变成:当我只能验收、不能设计的时候,靠什么保证质量。 一期给出的答案是,不接受任何形式的间接证据。
Personal Agent 最终部署在一台同时承载个人主页的 ECS 上。我完成了系统盘自定义镜像、加密复制和 KMS 加密盘迁移,配置独立域名与 HTTPS,让智能体和财务 MCP 分别运行在隔离的服务用户下,并保证数据库和 MCP 不直接暴露公网。每次修改 Nginx、系统服务和磁盘之后,原有个人主页都必须继续可用。
之后又建立了加密异地备份:两个 SQLite 数据库先做在线快照和完整性检查,再通过 restic 写入私有 OSS;备份仓库密钥与业务数据密钥分离,并保留离机副本。真正的验收不是看到一条“备份成功”,而是在另一台 Mac 上从 OSS 拉回快照,用离机密钥解密数据,检查两个数据库的结构、引用和回执关系,再以专用只读模式启动服务。
这条链路同样经历了“测试全绿,真实环境失败”,而且失败的方式恰好证明了上面那条原则。
第一次部署需要六处修复才跑通。最关键的一处是:备份用户可以列出暂存目录,却打不开里面的任何文件——setgid 缺失叠加 UMask=0077 和一个硬编码的 0600。而我的验证脚本检查了 ls 能否列出目录,却没有真的以备份用户的身份读一个文件。一次针对目录的断言,替代了一次针对能力的断言,于是 55 项检查全部通过,备份却是空的。
第一次异机恢复也一样:数据库和解密检查都通过了,后续审查却发现所谓的“只读启动”仍然组合了正常的恢复路径,存在修改还原库、甚至访问飞书的可能。修复权限和专用只读入口之后,我重新生成快照并完整复验,检查项从 55 项增加到 66 项,全部通过,异机恢复演练重新跑通。
最后还做了一次完整回滚:关闭写入,停止服务,移除智能体的 Nginx 入口和 systemd 单元,保留数据与密钥,再重新安装、恢复服务、恢复 Nginx 并重新打开写入。整个过程中,个人主页持续接受可用性检查,恢复前后的数据库计数和外部记录保持一致。
这部分工作花费的时间远超最初预期,也最直接地区分了玩具和产品。
一个玩具的完成标准是功能在本地跑通;一个真实产品还要面对公网、身份、权限、进程重启、服务启动顺序、密钥丢失、磁盘损坏、数据恢复和回滚。1995 项测试不是这些问题的替代品,最多只是进入真实环境的门票。
亲手搭一遍,和拖出一个演示原型不是同一件事
到这里应该说一下代价。
这是我这段时间全力投入的项目,而真实的过程远没有前面几节读起来那么顺。16 天里更多的时间不是在往前走,而是在原地反复:一个已经宣布完成的模块,被独立审查退回,六个阻塞级缺陷;一份写好的方案,因为一次真实调用的结果不对,推倒重来;一次部署改了六处才跑通;一次异机恢复通过之后,又因为“只读”不是真的只读而重做一遍。多次“已经完成”的判断被我自己推翻——每一次推翻都意味着前面几天的进度作废。
而同样一个“能记账的智能体”,用 Dify 这类编排平台,一个下午就能跑通演示原型。
我不认为那是错的。那类工具的目标是快速验证想法,它在那个目标上做得很好,我自己也用它验证过东西。真正的差别不在于最后谁跑得起来——两个都跑得起来——而在于这篇文章里每一个真正花掉我时间的问题,在那个模式下根本不会出现。
5 秒的默认读取超时、setgid 缺失叠加 UMask=0077、被污染的会话让模型学会假装记账、一个组合了恢复路径因而并不只读的“只读入口”——这些问题不会因为换了工具就消失,它们只是变成了别人的默认值,藏在我不拥有的那一层里,直到某天以我看不懂的形式浮上来。
这就接回了这篇文章从头到尾在讲的那件事:我的价值在那些判断上。而在一个你不拥有的层里,你没有可做的判断——你可以配置它,但你不能决定它。前面那些决定,无论是把成功绑定到飞书回读,还是拒绝会话列表,还是在中途停下来先补底座,都必须建立在“这一层归我”的前提上。用编排平台,我会是一个永久的执行者。
所以代价是真实的:时间、反复,以及大量我本来可以不必知道的知识。换回来的是,这套系统的每一条边界都是我自己划的,出问题时我知道该去哪里看——以及,我现在对智能体这套东西的理解,和之前拼几个演示原型时完全不是一个量级。
这两件事都值这个价。
回看一期,我真正负责的是决策和完成标准
大量代码、测试和运维步骤由编码智能体完成或指导,但一期并不是把一句需求交给 AI,然后等待项目自动长出来。
我把范围冻结在财务,没有让知识库、健康、衣橱和资产一起进入一期;我把 iPhone 定义成对话和确认入口,把高权限能力留在服务端;我拒绝了会话列表这套主流交互,让服务端自己消化上下文限制,而不是把它转嫁给用户;我选择 ADK,但把业务策略、MCP 合同、状态和审计留在框架之外;我没有接飞书现成的公开 MCP,而是自己设计每一个业务工具的边界;我在开发中途先补所有数据域共用的智能体底座,改变了原来的开发顺序;我关闭没有真实证据的批量写入;我把成功绑定到飞书回读,而不是模型文本;我决定哪些失败自动恢复,哪些位置必须把控制权交还给人。
很多产品判断并不表现为新增一个页面或功能。它们更经常表现为暂停、改顺序、拆概念、放弃一个现成的方案、关闭一条已经能跑的路径,或者拒绝接受一个看起来已经通过的验收结果。
回头看,一期最贵的三个决定都是“不做”:不用会话列表,不用编排平台,不接现成的公开 MCP。三次拒绝的理由是同一个——接受那个现成的东西,就等于放弃一个我必须自己拥有的判断。 省下的时间是真的,交出去的控制权也是真的,而后者要等到出事的那天才收费。
还有一件事是这个项目独有的。我做开放平台十年,一直站在平台方:定权限范围、定错误码、定审计口径,然后由接入方去承担这些规则的成本。这一次三个角色都是我——我写下“每一次写入都必须写后回读”,然后自己去处理它带来的超时;我要求“存在重复候选时必须零写入”,然后自己在手机上一条条确认。第一次为自己定的规则付账,是一期给我的东西里最难从平台一侧学到的。
编码智能体可以持续生成下一段代码,但它不会天然知道当前正在实现的是一个局部功能,还是一个会冻结未来几年产品形态的抽象。它也不能替我承担一次真实写入、一次数据丢失或一次高风险操作的结果。
这仍然是产品 Owner 的工作。
下一步:让 Personal Agent 开始编排编码智能体
一期虽然是用编码智能体完成的,但开发过程仍然高度依赖我坐在电脑前不断敲字。
我需要提交下一项任务,等待实现,要求运行测试,把结果交给另一个智能体审查,再把审查意见送回去修复。上下文、SHA、验收标准、失败原因和当前状态散落在聊天、终端、仓库和我的记忆里。围绕 CC Switch、LiteLLM 和 CCR 的一系列尝试处理了模型与供应商路由的一部分摩擦,却没有接管整个开发过程的状态。
因此,一期之后的下一项目标不是立即增加新的个人数据域,而是开始搭建开发智能体闭环(Development Agent Loop),也就是一套图式编排循环。
我希望把现在已经人工跑通的流程变成一张显式任务图:
在 Personal Agent 中提交需求
→ 生成 PRD、技术方案、任务拆解和验收标准
→ 我在手机上修改或批准计划
→ Home Mac Worker 创建隔离工作树(worktree)并执行编码
→ 运行确定性测试和安全检查
→ 独立编码智能体审查当前代码差异与证据
→ 有边界地修复、复测和再次审查
→ 生成候选 commit 和 PR
→ 我确认最终合并
任务图拥有状态、依赖、重试、暂停、恢复和模型交接;模型只是图中的执行节点。模型额度耗尽、供应商失败或某个节点输出不合格时,系统应该保存证据并停在明确状态,而不是让我重新解释整个项目。
我仍然会出现在需求、产品歧义、高风险动作和最终合并这些位置。但我不需要再一直守在电脑前,用一条接一条消息维持整个开发循环。
第一期,我用编码智能体做出了 Personal Agent。
下一阶段,我想让 Personal Agent 开始编排编码智能体,持续迭代它自己。