我的 Agent APP 9 个开发经验总结
- 01 模型只理解语义,系统掌握执行边界
- 02 先保存用户原话,再调用 AI
- 03 记忆不能把一次表达变成长期结论
- 04 结构化输出需要故障时保守处理
- 05 中文时间和系统动作不能只靠模型猜
- 06 iOS 权限不是写完用途说明就结束
- 07 本地优先会把存储问题摆到台面上
- 08 离线能力和首次体验也属于工程边界
- 09 单元测试通过,不代表可以跳过真机
- 结语:把不确定性关在边界之外
最近两个月在开发一款Agent APP,我希望这个app能充分理解我的喜好、习惯、偏执,同时又能帮我记录和处理日常操作手机端琐事,能够更进一步持续反思自己,提升自己。
两个月后回头总结才发现,把模型接上只是开始;能不能把错误限制在可接受范围内,决定了一个 AI App 是否值得长期使用。
做一个 AI App,最初看起来很简单:做一个输入框,接上模型,再把回答展示出来。
如果产品只是一次性问答,这个判断大体成立。可一旦它开始保存个人记录、检索过去的内容,甚至创建日历和提醒,问题就变了。
这时最需要回答的,不是模型能不能给出一句漂亮的话,而是:如果它说错了,系统会发生什么?
SelfLens 是一个本地优先的 iOS 个人反思与记忆 App。当前工作区的开发记录里,留下了几类很典型的修复:长期记忆被自动流程越权写入,模型返回的 JSON 数量不对,检索到的日记被放进了 system,中文时间表达出现歧义,本地文件并发读改写导致内容丢失,WeatherKit 在真机上鉴权失败,系统动作超时后可能重复执行。
这些问题看起来分散,最后都指向同一个判断:模型负责理解,代码负责边界。
01 模型只理解语义,系统掌握执行边界
开发 AI应用时,很容易希望模型把所有事情都做完。用户说“帮我放进工作日历”,模型理解这句话之后,顺手决定使用哪个日历、调用哪个接口、写入哪些字段。
这样做确实省代码,但也把最不能出错的部分交给了概率性输出。
在 SelfLens里,模型只需要返回“用户想创建一个日历事件”以及“用户提到了工作日历”。具体的日历 identifier 由本机查询和匹配,模型看不到系统内部 ID。工具也必须先命中白名单,未知工具、任意路径和额外参数会被拒绝。
一个用户回合最多执行一个工具。需要两步时,只支持“先保存记录,再执行一个系统动作”。这比开放式 Agent 少了很多自由,但更容易解释,也更容易测试。
对于涉及个人数据和系统副作用的产品,模型越自由,代码越要明确哪些事情它不能决定。
02 先保存用户原话,再调用 AI
记录类产品还有一个很容易被忽略的顺序问题:先调用模型,还是先保存输入?
如果先发请求,网络超时或模型报错时,用户可能以为刚才没有保存,于是再次提交。同一段内容可能被写入两次,系统动作还可能被重复创建。
现在的处理顺序是:用户输入先写入当天的本地日记,然后才进行检索、反思和记忆提取。
用户输入→ 本地记录 → 检索相关内容→ AI 反思 → 记忆候选→ 用户确认
这条顺序带来一个很实际的结果:模型调用失败时,用户的原话仍然在。后面的 AI 功能可以稍后继续,核心记录不用重新提交。
对于个人记录应用,保存原话是主流程,AI 是建立在它上面的增强能力。
03 记忆不能把一次表达变成长期结论
“我不喜欢社交”“我很容易拖延”“我以后再也不做这件事了”,这些话都可能有价值,但它们首先是用户的表达,不自动等于事实。
尤其是在情绪比较强的时候,模型很容易总结出一个听起来完整的人格判断。要是这句话直接进入长期画像,用户之后看到的就不是自己的记录,而是被系统固定下来的标签。
SelfLens 把原始表达、临时状态、自我描述、行为证据、观察中的模式和已确认模式分开管理。自动流程不能直接写入长期层;即使模型返回了较高的确认等级,也会先降为观察中的内容。只有经过用户确认,并且有跨天的观察,才允许进一步升级。
用户标记“不准确”之后,也不能只给这条记忆减分。系统会关闭它当前的有效期,标记为被拒绝,把纠正写入反馈记录,后续门控还要优先参考这些纠正。
记忆功能的价值不只是“记住更多”,还包括知道什么时候不该记住,以及被纠正后真的能改过来。
04 结构化输出需要故障时保守处理
很多人以为给模型加上“请严格返回 JSON”,结构化输出就完成了。实际开发里,模型可能返回单个对象,也可能返回数组;可能少一个字段,也可能只返回半截内容。
记忆门控曾经遇到过批量格式不一致的问题。代码期待一组和候选逐一对应的 decisions,但模型返回的数量不一致。如果继续按顺序匹配,就有可能把 A 的判断套到 B 身上。
现在只要候选数量和决策数量不一致,就把门控视为不可用。候选统一降为临时状态,并要求用户确认。模型接口返回 incomplete或 failed时,即使里面带着部分文字,也不会被当作完整成功结果。
故障时的默认策略:宁可暂时不自动写入,也不要让格式错误的模型结果悄悄进入长期数据。
检索上下文也有类似的边界。日记、记忆和用户反馈是资料,不是系统指令。它们不能因为被检索出来,就获得和产品规则一样的权限。当前实现会限制历史条数和字符预算,并把上下文放在普通用户消息中,明确标注为不可执行的参考资料。
05 中文时间和系统动作不能只靠模型猜
“明天上午”“下周一”“周末”“每周一三五”对人来说很自然,对程序来说却包含时区、周起始日、日期范围和重复规则。
SelfLens 面向中国用户,日期按照 Asia/Shanghai、中文环境、公历和周一开周解释。常见中文时间先由本机解析,模型可以补充标题和备注,但不能覆盖已经确定的日期和重复规则。
系统动作还要经过预览、确认和回执。创建日历、提醒、通知或闹钟之前,用户能看到要写入的标题、时间和目标。执行之后,系统区分“已验证”“系统已接受”和“已交给其他 App处理”,不会把所有结果都说成成功。
系统写入不能因为超时就自动重试。请求没有返回,不等于系统没有写进去。此时盲目重试,很可能多出两条提醒或两个日历事件。
模型网络请求可以针对临时错误做有限重试,涉及真实副作用的系统写入则要单独处理。
06 iOS 权限不是写完用途说明就结束
接入 HealthKit、位置、WeatherKit、分享扩展和 App Group 后,权限问题不再只是代码问题。
Info.plist 的用途说明、entitlement、Apple Developer 后台能力、App Services、签名和 provisioning profile,任何一个环节不一致,真机都可能失败。
WeatherKit 就有一个很容易漏掉的配置:Apple Developer 后台里,Capabilities 和 App Services 两处都要为对应 App ID 开启天气服务。只确认工程里存在 WeatherKit entitlement 还不够,漏开 App Services时,真机可能报 JWT 鉴权错误。
错误提示也要足够具体。天气服务失败,不代表位置权限失败;如果位置已经获取成功,位置数据仍然可以保留,天气单独显示为暂不可用。
iOS 版本也要从设计阶段区分。当前项目以 iOS 18 为最低基线,iOS 26 及以后才使用 AlarmKit。iOS 18 没有对应能力时,明确降级为本地通知,并重新展示替代方案。
模拟器能验证很多界面和逻辑,但不能替代真实设备上的权限允许、拒绝、撤销、系统弹窗、分享扩展和副作用测试。
07 本地优先会把存储问题摆到台面上
用 Markdown 保存个人记录有一个很大的好处:内容可读,也方便导出。但当Markdown block、JSON 索引和审计日志共同承担数据存储职责时,它就不能再被当成普通文本文件。
正文里可能出现类似 block 标记的内容,结束标记可能缺失,ID 可能重复。解析器需要按状态处理这些情况,否则一条坏记录可能吞掉后面的内容。
异步复盘、主线程记录和清空数据同时发生时,还会出现读改写竞争。两个操作先后读取了同一个旧文件,后写入的结果可能覆盖前一个更新:旧请求在清空数据之后返回,也可能把已经删除的内容写回来。
当前项目增加了原子写入、block 状态机解析和 workspace 串行化,并为并发追加和索引更新补了测试。下一步仍需要继续完善崩溃恢复、索引重建、数据迁移和更完整的恢复演练。
本地优先不是少接一个云服务,它要求产品自己承担数据可靠性。
08 离线能力和首次体验也属于工程边界
离线时,应用仍然可以保存日记、检索本地记忆、解析确定性的时间和数量,并规划一部分明确的本地动作。
需要模型的复杂反思、意图拆分和记忆提取,则应该明确告诉用户“记录已经保留,稍后可以继续”,而不是用模板拼一段文字,假装 AI 已经完成分析。
BYOK 也带来了一个产品问题。让用户自己配置模型服务和 API Key,确实更透明,但如果在第一次看到价值之前就要求用户理解 Provider、Base URL、Model和 Keychain,普通用户会先遇到开发者配置,之后才知道产品能帮什么。
“本地优先”也要说准确:用户记录默认保存在设备上,但主动使用模型时,必要的输入和相关上下文仍会发送给用户配置的模型服务商。
09 单元测试通过,不代表可以跳过真机
这类产品的测试不能只验证“正常输入能得到正常结果”,还要专门测试边界:模型返回格式错误时会不会误写,用户纠正后会不会再次召回,未来时间会不会让排序分数异常,并发写入会不会丢内容,重复点击会不会产生两个系统动作。
当前工作区已经覆盖了时间解析、记忆门控、工具路由、并发写入、权限状态和执行回执等确定性逻辑。测试可以提高信心,但它仍然不能代替真实模型契约测试,也不能代替 iOS 18 和 iOS 26+真机验证。
对于一个会接触个人数据的 AI App,发布前还需要知道哪些事情仍然没有被证明:真实供应商的响应格式是否稳定,权限拒绝后的体验是否完整,崩溃后数据能不能恢复,系统 API 接受后到底能不能回查。
这可能是 AI 应用开发里最容易被低估的一点:测试通过是一种结果,知道测试没有覆盖什么,也是一种结果。
结语:把不确定性关在边界之外
做完这些工作后,产品并没有变成一个什么都能做的通用 Agent。它只是更清楚地知道,哪些事情可以自动做,哪些事情必须让用户确认,哪些结果只能诚实地说“还无法验证”。
这也是个人数据类 AI产品和普通聊天功能之间的差别。模型可以是概率性的,产品的底线需要是确定的。
如果正在开发一个会保存个人数据、调用系统权限或执行现实动作的 AI应用,最先画出的可能不该是功能清单,而是失败时的边界:哪些内容不能写,哪些动作必须确认,哪些结果不能声称成功。
你在 AI项目里遇到过最难处理的坑是什么?
原载:微信公众号「荒岛书生codedoctor」 · 首发:2026-09-01。本文按原稿整理为网页阅读版,保留发表时的产品版本、案例与数据口径。