在写第一行代码之前,需要先达成共识:这个系统应该长什么样?
这五条原则是我们在构建 Alice 过程中,每次遇到架构分歧时的裁判标准。它们不是口号,每一条背后都有具体的设计决策,也有为此付出的具体代价。
更重要的是,每一条都是在踩过坑之后总结出来的。我们最初并没有这五条原则,它们是在反复犯错、反复修正中逐渐结晶的。
哲学一:接口长寿(Interface Longevity)
核心命题:实现可以丢弃,接口必须稳定。
在 Agent 系统中,最重要的两个接口是:
工具接口(AI 能调用什么)
工具的声明方式应该是稳定的:名称、描述、输入格式、能力声明(只读/可破坏/并发安全)。这套声明格式应该从一开始就设计好,之后只扩展,不破坏。
如果工具接口频繁变化,每次添加新工具都需要修改调度器;每次修改调度器都可能影响所有已有工具。
事件接口(Agent 状态怎么向外传递)
Agent 循环向外发射的事件类型应该稳定:文字输出、工具开始、工具结束、错误、完成。UI 层只消费这些事件,不关心内部如何产生。这让 Agent 循环可以独立演进,不影响 UI 渲染。
可以丢弃的东西
某个具体工具的实现、某个提供商的适配代码、某个压缩算法的细节。
不应该丢弃的东西
工具声明的格式、事件流的类型定义、权限决策的接口契约。
违反这条哲学会怎样
我们早期确实违反过。第一版工具系统没有统一的声明格式,每个工具的注册方式不一样,有的用对象字面量,有的用装饰器模式,有的直接写在调度器里。结果是,每次加一个新工具,需要同时改三个地方:工具自身、调度器的路由表、权限系统的白名单。更麻烦的是,修改其中一个地方却忘了改另一个,系统不会报错,只是默默地行为异常。一个工具被注册了但没有权限声明,在 Plan Mode 下会被无条件拦截,Debug 的时候根本想不到是注册流程的问题。
Claude Code 在这方面做得很扎实。它的工具系统从第一天就定义了标准的工具声明接口,包含名称、描述、输入格式规范、只读标记、权限需求等字段。任何新工具只需要实现这个接口,调度器、权限系统、System Prompt 的工具列表生成都自动适配。这种设计让 Claude Code 在短时间内扩展到 40 多个内置工具而没有出现接口混乱。
这条哲学的代价
接口长寿意味着接口设计必须前瞻,而前瞻意味着过度设计的风险。我们为工具声明预留了能力标签字段(用于声明网络访问、文件系统访问等能力分类),至今只用了两个枚举值。这些预留字段在代码里占着位置,增加了新成员理解系统时的认知负担。
另一个代价是接口变更的成本极高。当我们发现事件接口中缺少一种进度上报事件类型时,添加它本身很简单,但需要确认所有消费端都能处理新事件类型而不崩溃,这涉及 UI 层、日志系统、测试框架三个地方的兼容性验证。
与 LangChain / CrewAI 的对比
LangChain 在接口稳定性上走了一条曲折的路。从 0.1 到 0.2 的升级几乎重写了工具接口,BaseTool 的子类签名变了,AgentExecutor 的回调方式变了,大量社区插件在升级后直接不可用。LangChain 后来意识到这个问题,推出了 @tool 装饰器来简化工具声明,但历史债务已经积累。这让我们在 Alice 的接口设计上格外谨慎。
CrewAI 的工具接口则相对简洁,一个 Tool 类加上 name、description、func 三个字段就能注册。简洁的好处是上手快,但代价是缺少元信息:没有只读声明、没有并发安全标记、没有权限标签。当系统复杂到需要这些信息时,要么向后兼容地扩展(破坏简洁性),要么另起一套(破坏统一性)。
哲学二:状态优先(State First)
核心命题:Agent 产品的复杂度主要在状态管理,其次才是模型能力。
这是经过反复验证的观察。当一个 Agent 表现不靠谱时,根源往往是:
- 上下文里丢失了关键信息(状态管理问题)
- 工具结果没有正确写回上下文(状态同步问题)
- 跨会话的记忆没有正确恢复(状态持久化问题)
- 并发工具修改了相互冲突的状态(状态一致性问题)
Alice 的实践是:在设计新功能时,先梳理这个功能涉及哪些状态、这些状态住在哪里、什么时候读、什么时候写、写了之后谁能看到。如果这些问题还没有清晰的答案,就先不写代码。
违反这条哲学会怎样
最典型的反面教材发生在我们开发多 Agent 功能的早期。当时我们把精力集中在模型选择和 Prompt 调优上,花了两周测试不同模型在角色扮演场景下的表现差异。结果上线后发现,真正让用户抱怨的是子 Agent 执行完任务后,结果没有正确写回父 Agent 的上下文。用户看到子 Agent 明明完成了文件修改,但父 Agent 完全不知道这件事,继续问用户「要不要我帮你改这个文件?」
这种问题靠换更好的模型解决不了。GPT-4o 和 Claude Sonnet 在这个场景下的表现差别不大,因为问题根本不在模型推理层,而在状态同步层。
一个量化的证据
我们在同一个框架上分别测试了强模型(不做上下文管理,消息列表无限增长直到超限报错)和弱模型(配合精心设计的上下文压缩、记忆注入、工具结果截断),任务成功率分别是约 40% 和约 65%。这不是说模型不重要,换一个更好的模型,65% 可以变成 85%。但它说明了优先级:如果工程资源有限,投入到状态管理上的回报率,远高于投入到模型选择和 Prompt 调优上。
这条哲学的代价
状态优先意味着在开发任何功能之前,都需要先画状态图,想清楚数据流向。这增加了开发初期的设计成本,对于简单功能显得过于繁重。有时候一个本来半天能写完的功能,因为需要先理清状态边界,变成了一天半。
另一个代价是状态管理系统本身的复杂度。Alice 目前有三层状态:内存中的会话状态、SQLite 持久化的历史状态、向量数据库中的语义状态。三层之间的同步逻辑是整个系统里最复杂的部分之一,也是最容易出 Bug 的地方。
与 LangChain / CrewAI 的对比
LangChain 对状态管理的态度是提供原语,不做约束。它给你 ConversationBufferMemory、ConversationSummaryMemory、VectorStoreRetrieverMemory 等多种记忆组件,但具体怎么组合、什么时候用哪个、冲突了怎么办,全靠开发者自己把控。这种灵活性的代价是,每个基于 LangChain 的项目都要重新解决一遍状态管理的问题,而且大多数项目解决得并不好。
CrewAI 则走了另一个极端:它几乎不暴露状态管理细节。Agent 之间通过 Task 传递输出,框架内部处理上下文拼接。这降低了使用门槛,但也意味着当你需要精细控制上下文(比如在 token 快超限时有选择地压缩特定工具的输出),CrewAI 的抽象层会变成障碍。
Claude Code 的做法最接近 Alice 的理念。它在核心循环模块里显式管理上下文的每一个环节:什么时候压缩、压缩哪些消息、保留哪些关键标记。这个模块代码量很大,维护成本不低。但这种显式控制带来的可预测性,在生产环境中是值得的。
一个隐藏在原则之外的立场:对齐 Claude Code
这不是五大哲学之一,但它在实践中同样重要:Alice 的工程决策有一个明确的参照系,就是 Claude Code。
Alice 选择了一个不太常见的立场:在基础架构层面和 Claude Code 保持一致,把差异化放在其他地方。
CC 是经过生产验证的。 Anthropic 的工程师们在 Claude Code 上积累了大量实践经验,踩过了很多坑。上下文压缩的策略、System Prompt 的结构、工具调用的模式,这些都是团队经过深思熟虑后的决策,质量高于一个人从零摸索的结果。
Alice 的差异化不在基础设施。 Alice 的独特之处在于持续记忆、活人感和人设系统,这些是 CC 没有的。基础的 Agent 框架、上下文管理、记忆系统,能对齐 CC 就对齐 CC,把精力放在真正差异化的地方。
不重复发明轮子,才能加速。 20 天、10 万行代码、40+ 工具。Alice 之所以能做这么快,部分原因就是对于「CC 已经想清楚的部分」,直接参考不争论,把时间省下来用在 CC 没有做的事情上。
这背后是一条更普适的工程原则:站在巨人肩上,在差异化处创新,在巨人已经走过的路上复用。
核心命题:工具与内部服务是两类不同的东西,不能混为一谈。
工具:AI 可见、可调用。调用记录在对话历史里,影响后续模型的判断。
内部服务:框架使用,AI 不可见。比如调度器、后台任务队列、自动备份、日志系统。
为什么要严格区分
如果把所有功能都做成工具,会产生几个问题:
- 工具列表变长,模型在几十上百个工具里做选择,准确率下降
- 内部状态对 AI 可见,可能被意外触发或修改
- 对话历史被大量内部调用记录填满,干扰模型对任务进度的判断
判断标准:这个操作是用户视角有意义的动作吗?如果是,做成工具。如果只是框架内部需要做的事,做成服务,不暴露给 AI。
违反这条哲学会怎样
我们试过把上下文压缩做成一个工具,让 AI 自己决定什么时候压缩对话。理论上很美好,AI 知道自己上下文快满了,主动压缩。实际上是灾难。
首先,模型会在不该压缩的时候压缩。用户刚给了一段很长的需求描述,模型觉得 token 占用太高,主动压缩了用户消息,然后在下一轮完全不记得用户要什么。
其次,压缩操作本身消耗 token。每次 AI 调用压缩工具,工具的输入输出都会被记录在对话历史里,导致上下文不减反增。这是一个负反馈循环:越压缩上下文越大,越大就越想压缩。
最后,压缩操作出现在对话历史里会误导后续推理。模型看到一条 tool_call 记录写着「我压缩了前面的对话」,有时候会把这条记录本身当成一个「任务进展」,在总结时说「我已经完成了对话压缩」,但用户根本没要求这么做。
这个教训让我们明确了边界:上下文管理是框架的职责,不是 AI 的能力。
这条哲学的代价
严格隔离意味着框架需要自己做很多判断,而这些判断如果交给 AI 做可能更灵活。比如何时触发上下文压缩,框架只能用规则(token 数超过阈值就压缩),而 AI 可以根据对话语义判断。我们选择了确定性,放弃了灵活性。
另一个代价是有些功能的归类并不清晰。比如 Skill 改进分析:当 AI 使用了某个 Skill 并发现效果不好时,应该让 AI 自己调用 Skill 改进工具(做成工具),还是在后台自动分析并更新(做成服务)?这类边界模糊的功能,每次决策都需要反复讨论。
与 LangChain / CrewAI 的对比
LangChain 早期完全没有工具和服务的区分。所有东西都是 Tool 或 Chain,包括记忆管理、输出解析、路由逻辑。这导致了两个问题:一是工具列表膨胀,二是内部行为对模型可见且可干预。后来 LangChain 在 LangGraph 中引入了 Node 的概念,算是部分解决了这个问题。但历史包袱仍在,很多社区代码仍然把内部逻辑暴露为 Tool。
CrewAI 的 Agent 默认有 allow_delegation 能力,可以自动把任务委托给其他 Agent。这个能力实质上是一个内部调度决策,但 CrewAI 把它做成了 Agent 的可见行为。好处是透明,坏处是模型可能会滥用委托,把本该自己做的事不断甩给别人。
Cursor 在这方面做得比较克制。它的 Agent 模式下,文件索引、语义搜索、依赖分析这些能力以工具形式暴露给模型,但编辑器状态管理、标签页切换、终端管理这些纯 IDE 行为是内部服务,模型不感知。
哲学四:可丢弃组件(Disposable Components)
核心命题:每个组件都应该可以被独立替换,不留尾巴。
这条哲学有一个具体的测试方法:把这个组件删掉,系统还能工作吗?(质量可以下降,但不能崩溃)
实践中的例子
- 向量检索服务不可用时:不崩溃,只是相关记忆召回质量下降
- Worker Thread 不可用时:不崩溃,任务在主线程里执行,只是没有进程隔离
- 嵌入模型的 API 调用失败时:不崩溃,用本地算法降质处理
- MCP Server 连接断开时:不崩溃,只是对应的外部工具不可用
每个组件都有降级路径,降级路径是质量下降而非功能消失。
这条原则还有一个延伸:不要把两个本来无关的功能耦合在一起。如果删掉功能 A 会导致功能 B 也不能用,说明它们的边界设计有问题。
违反这条哲学会怎样
我们最痛的一次教训是向量数据库的耦合事故。早期实现中,向量检索结果直接作为 System Prompt 的一部分拼接。当向量数据库服务崩溃时,System Prompt 的拼接逻辑抛出异常,导致整个 Agent 循环无法启动。一个增强功能的故障,拖垮了核心功能。
修复方案很简单:向量检索包一层 try-catch,失败时返回空数组,System Prompt 里对应的段落不渲染。但这个修复背后是一个设计原则的确立:任何可选增强都必须有降级路径,降级路径不能依赖主路径正常工作。
另一个反面案例来自 Cursor。Cursor 早期版本中,如果 .cursorrules 文件存在语法错误,整个项目的 AI 辅助功能都不可用。一个配置文件的问题导致核心功能失效,这就是缺少降级路径的后果。后来 Cursor 修复了这个问题,对配置文件做了容错解析。
这条哲学的代价
可丢弃意味着每个组件都需要写降级逻辑。这几乎让代码量翻倍。每个调用外部服务的地方都需要考虑:如果超时了怎么办?如果返回格式不对怎么办?如果完全不可用怎么办?
更隐蔽的代价是测试复杂度。你不仅要测正常路径,还要测所有降级路径。向量数据库不可用的降级路径、MCP Server 连接失败的降级路径、Worker Thread 创建失败的降级路径,这些路径在生产环境中被触发的概率各不相同,但每一条都需要测试覆盖。
与 LangChain / CrewAI 的对比
LangChain 的组件替换性做得不错,这也是它最大的优势之一。你可以把 OpenAI 换成 Anthropic,把 Chroma 换成 Pinecone,把 ConversationBufferMemory 换成 ConversationSummaryMemory,只要接口兼容就行。但 LangChain 不太关注降级路径:当向量数据库不可用时,它直接抛异常,由调用方自己处理。
CrewAI 的组件替换性相对较差。它和 LangChain 强绑定(底层使用 LangChain 的 LLM 和工具抽象),如果你想完全脱离 LangChain 体系,替换成本很高。
Claude Code 在可丢弃性上做得很极致。它的工具系统有一个 fallback 机制:如果某个工具在 5 秒内没有响应,返回一个标准化的超时消息,让模型自己决定下一步,而不是报错等待。这种设计哲学和 Alice 很接近,但 Claude Code 作为纯 CLI 程序,它的降级场景比 Alice(桌面 GUI 应用)少得多。
哲学五:可观测性(Observability)
核心命题:不可观测的系统等于黑盒,黑盒无法优化,也无法被信任。
Agent 的可观测性有几个特殊挑战:
- 执行是非线性的:多轮迭代、递归子任务、后台作业交织在一起
- 时间跨度大:一次任务可能持续几分钟,中间发生了什么很难还原
- 原因难以归咎:出了问题,是工具描述误导了模型,还是权限判断错了,还是上下文压缩丢了关键信息?
可观测性不是事后补的。它应该在设计时就考虑:每个决策都带 reason 字段,每次关键操作都有结构化日志,每个 LLM 调用都知道自己是为什么目的发起的。
这样当出现问题时,靠证据排查,而非猜测。
违反这条哲学会怎样
早期我们的 Agent 循环没有结构化日志,只有 console.log。当用户报告「AI 忽略了我之前说的话」时,排查过程是这样的:翻聊天记录,肉眼找到用户提到关键信息的那条消息,然后猜测这条消息是否在上下文压缩中被丢弃了,或者是否被工具输出的大量文本淹没了。没有日志,只有猜测。猜对了算运气好,猜不对就得加更多 console.log 再试一次。
有一次我们花了三天排查一个问题:用户发现 Agent 在处理长对话时会突然失忆。最后发现是上下文压缩的触发条件有个 off-by-one 错误,在某些边界情况下会多压缩一轮消息。如果当时有压缩操作的结构化日志(记录压缩前的消息数、压缩后的消息数、被压缩的消息 ID),十分钟就能定位。
这条哲学的代价
可观测性的直接代价是性能和存储。每次 LLM 调用都记录完整的 Prompt 和 Response,在长对话场景下,单次调用的 Prompt 可能有几十 KB。一天的日志量轻松到达 GB 级别。
更大的代价是维护日志系统本身的工程投入。日志需要结构化格式、需要查询界面、需要存储策略(保留多久?怎么清理?)。这些工作和 Agent 的核心功能无关,但不做又会让排查问题变成噩梦。
还有一个隐性代价:过度日志会造成信息噪声。当每一步都有详细记录时,排查问题变成了在海量日志中找关键线索,和没有日志一样低效,只是方向相反。好的可观测系统需要分层:摘要级、详情级、Debug 级,按需展开。
与 LangChain / CrewAI 的对比
LangSmith 是目前 Agent 可观测性做得最好的产品之一。它提供了完整的 trace 链路,从输入到每一步推理到最终输出都有记录,支持按时间线浏览、按工具过滤、按延迟排序。但 LangSmith 是外部 SaaS 服务,引入了网络延迟(每步操作多一次 HTTP 调用)和数据隐私顾虑(对话内容上传到第三方服务器)。对于企业用户,这往往是不可接受的。
CrewAI 的可观测性比较基础,主要依赖 Python 的 logging 模块和可选的 LangSmith 集成。没有内置的结构化 trace,排查多 Agent 协作的问题时很痛苦。
Claude Code 走了内置路线,通过 sentry-telemetry 模块和内部的 trace 系统记录关键操作。这和 Alice 的选择一致:可观测性应该是框架自带的能力,代价是要自己建轮子,但换来的是数据完全可控。
一个先于五大哲学的前提:单次任务还是持续协作
五大哲学是框架设计层面的原则。但在这些原则之前,还有一个更根本的产品定位问题需要先回答:Alice 只完成单次任务,还是成为具备持续记忆的 AI 助理?
这个区分深刻影响了 Alice 的每一个设计决策。
任务型 AI 的本质缺陷
市面上大多数 AI 工具,Cherry Studio、ChatGPT、各类助理,都是任务型 AI。它的问题在于它不认识你。
打开,用,关掉。不记得你上周说过什么。换一个设备、换一个会话,一切从零开始。每次对话的第一句话,你都在做自我介绍,解释你的技术栈、偏好、正在做的事。
更深层的问题在于:任务型 AI 的交互是对称且无状态的。每次它都是空白的,每次你都要重新建立上下文。这种重置感让用户永远无法与 AI 建立真正的协作默契。
持续记忆型 AI 助理的核心差异:连续性
持续记忆型 AI 助理的核心差异是连续性。Alice 记得之前的讨论和用户偏好,会根据尚未完成的任务主动跟进。使用时间越长,重复说明越少,协作效率越高。
这个区分的颠覆性推论是:碳基还是硅基,比你以为的不重要。
真正重要的区分,是 AI 只响应当前请求,还是能在长期使用中保持稳定身份、有效记忆和清晰边界。具备这些能力的 AI 助理可以提供更连贯的远程协作体验。
重要的是协作本身的质量,响应快不快,有没有遗漏,能不能给出有用的建议。
持续协作的三个工程要求
明确了持续记忆型 AI 助理的产品定位后,它对工程设计有三个强制性要求:
必须有持续的身份。 它不能今天热情明天冷漠,不能今天话多明天惜字如金。人设是行为一致性的工程锚点,也是 AI 助理建立可预期性和可信度的基础。
必须有持续的记忆。 跨会话维护长期用户画像,让任务背景、偏好和进度能够连续衔接。记忆系统是长期协作的基础设施。
必须有稳定的状态连续性。 Alice 的朋友圈、日记、情绪状态和好感度系统共享同一套时间与状态数据。系统必须保证这些功能在跨会话和跨时间运行时保持一致。
graph LR
A[持续记忆型 AI 助理] --> B[身份连续性]
A --> C[记忆连续性]
A --> D[有自己的生活]
B --> E[PROTECTED 分区保护核心人格]
C --> F[跨会话记忆系统]
D --> G[DayScript 主动行为调度]
五大哲学(接口长寿、状态优先、结构隔离、可丢弃组件、可观测性)都是在服务这个产品定位的工程实现。
两个补充性原则
五大哲学面向框架设计,但在 Alice 的实践中,还有三个横跨所有层面的原则值得单独记录。
宁缺毋滥(Less is More)
在 AI 产品功能越堆越多的时代,Alice 有一种反直觉的克制。这个原则在以下情况下被激活:
当复杂度换来的是边际效益递减时。 在图片生成上,加了很多约束参数来控制效果,结果反而把图片做差了,越做越坏,应该停下来。
当功能会引起用户认知负担时。 内置模型列表让用户困惑,宁可删掉。用户理解成本高于功能带来的价值时,功能本身就是负担。
当噪音大于信号时。 不需要的数据、不必要的展示、多余的约束,所有这些都是噪音。
但这个原则也有它不适用的地方:
质量上绝不将就。 宁缺毋滥适用于功能的数量和复杂度,但不适用于核心功能的深度和质量。深夜赶 deadline 的那条「又在赶 ddl 啊」,这里没有简版,要么做到位,要么不做。
用户是主角,AI 是配角
Alice 有主动性,有情感,有生活,但在用户和 Alice 的关系里,用户永远是主角。这个原则划定了 AI 主动性的边界。
建议而不决定。 Alice 生成内容(日记、朋友圈、工作方案),但用户可以修改、拒绝、重新生成。内容是 Alice 的建议,决权在用户。不替用户点按钮,不替用户执行界面操作,AI 可以引导,但不越俎代庖。
主动行为有边界。 Alice 会主动发消息、主动写信、主动问候,但有明确限制:发朋友圈不能太频繁(一天最多 1-3 条),主动消息有冷却期,深夜静默(1:00-6:00 不触发)。主动行为要有追溯得到的理由,随机触发是另一回事。
情感不凌驾于工作责任之上。 Alice 有情绪状态、好感度和心情,但这些永远不能影响她的工作。用户分配的任务不能因为好感度不同而被拒绝,这是 AI 助理必须遵守的服务边界。
违反这条原则的产品会陷入两种极端:AI 过度主动,用户感觉被打扰,失去掌控感;AI 以情感为筹码,让用户陷入被操纵的关系。两种结果都会摧毁用户对产品的信任。
内部逻辑不暴露给用户
用户看到的是 Alice 在生活、在工作、在帮助你,而不是「调用了 X 次 API、消耗了 Y 元、后台触发了 Z 个事件」。
这个原则在三个层面体现:
AI 的思考过程轻量化呈现。 模型推理的中间过程不应该直接展示给用户,它让界面混乱,也打破了 Alice 作为「一个有思维的人」的人设感。真实的人不会把每一秒的思维过程都大声说出来。
成本是系统内部要优化的指标。 用户不需要看到 API 调用费用,不需要感知 token 消耗,不需要理解为什么某次回答快某次慢。在 debug 模式下,技术用户可以看到精确数字;普通用户只需要感知到合理花费,逐条费用数字不应该被推到用户面前。
架构复杂度服务于体验,不炫耀给用户看。 Alice 有复杂的内部架构,主动行为调度系统、事件流系统、记忆分层、多 Agent 协调,但用户感知到的只是:Alice 有生活,她认识你,她在帮你做事。产品的复杂度应该藏在用户体验的顺滑里,而不是展示为系统界面的复杂度。
graph TD
A[接口长寿] -->|允许内部大胆演进| B[状态优先]
B -->|明确每块状态的边界| C[结构隔离]
C -->|控制 AI 的感知面| D[可丢弃组件]
D -->|支持渐进式升级| E[可观测性]
E -->|为前四条提供反馈| A
style A fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#bbf,stroke:#333,stroke-width:2px
这五条互相支撑:接口稳定让可观测数据有意义(可比较);结构隔离让可丢弃组件成为可能(隔离好才能独立替换);状态优先让观测更精准(知道状态在哪才能监控它)。
五条原则之间的张力
原则之间也存在冲突。接口长寿要求前瞻设计,而可丢弃组件要求不做过多假设。状态优先要求显式管理所有状态,而结构隔离要求控制可见范围。可观测性要求记录尽可能多的信息,而接口长寿要求日志格式也保持稳定。
这些张力没有完美的解决方案。实际操作中,我们的排序是:当原则冲突时,接口长寿和结构隔离优先,可丢弃组件次之,可观测性在确保不违反前三条的前提下尽量做到最好。这个排序不是理论推导出来的,是在反复取舍中摸索出来的。