构建可扩展 AI Agent 系统的三次演进
构建可扩展 AI Agent 系统的三次演进
I. 2025年12月的某天
我们要添加一个新功能:群体讨论(discussionChat)。
这应该很简单。我们已经有 interviewChat 了——一对一访谈,用户和 AI 模拟的 persona 深度对话。群体讨论只是从 1对1 变成 1对多:3-8个 persona 同时参与,观察他们的观点碰撞。
理论上,只需要:
- 复用 interview 的逻辑
- 改改提示词,让 AI 模拟群体对话
- 改改 UI,显示多个发言者
但现实是:我们需要改动 12 个文件。
更糟的是,我们发现了这个:
三个几乎完全相同的 Agent wrapper。 每添加一个功能,都要在三个地方复制粘贴。 每修一个 bug,都要改三次。
那一刻我们意识到:有些东西从根本上错了。
不是代码不够优雅。 不是没有抽象。 而是我们在用传统软件工程的思维构建 AI Agent 系统。
这篇文章记录我们如何走出这个困境——通过三次架构演进,重新思考 AI Agent 应该如何构建。
II. 重新思考:什么是 AI Agent?
在动手重构前,我们停下来问了一个根本问题:
AI Agent 和传统软件有什么本质区别?
传统软件的世界
传统软件基于状态机:
这个模型的核心假设:
- 状态是显式的:我知道当前在哪个状态
- 转换是确定的:给定状态和事件,下一个状态唯一
- 控制是精确的:if-else 覆盖所有路径
这在传统软件中很好。但在 AI Agent 中?
AI Agent 的世界
LLM 不是这样工作的:
这里的"状态"在哪里?
- 不在一个
state字段 - 而在整个对话历史中
AI 从对话历史推断:
- 用户想做什么研究?
- 目前进展到哪里?
- 下一步应该做什么?
这是完全不同的范式。
三个核心洞察
从这个观察出发,我们得出了三个洞察,它们塑造了我们的架构演进。
洞察 1:对话即状态
传统做法:维护显式状态
AI-native 做法:从对话推断状态
为什么对话优于状态机?
- 天然对齐:LLM 的工作方式就是基于消息历史
- 容错性强:状态机出错时难恢复;对话可以"倒带"、重播
- 易扩展:添加新能力不需要修改状态图
洞察 2:推理与执行分离
人类做决策:
- 理解意图:"我要做什么?" → 明确目标
- 选择方法:"我怎么做?" → 执行步骤
AI Agent 也应该如此:
为什么分离?
- 推理需要深度思考(用 Claude Sonnet 4)
- 执行需要快速响应(可用更小模型)
- 关注点分离,单一职责
洞察 3:简单胜过精确
面对"AI 健忘"问题,我们可以:
方案 A:Vector DB + Semantic Search
- ✅ 精确检索
- ❌ 需要 embedding、索引、复杂查询
- ❌ 维护成本高
方案 B:Markdown 文件 + 全文加载
- ✅ 简单透明,用户可编辑
- ✅ 依赖大 context window(Claude 200K tokens)
- ✅ 更易调试、理解
我们选择了方案 B。
为什么?
- Context window 改变了游戏规则:用户记忆通常 < 10K,全文加载完全可行
- 简单方案更可靠:没有 embedding 不一致、没有检索失败
- 用户可控:记忆透明,用户可查看、编辑
四个设计原则
从这三个洞察,我们提炼出架构的核心原则:
1. Messages as Source of Truth
- 所有重要信息都在消息中
- 数据库只存派生状态(如 report、studyLog)
- 类似 Event Sourcing:消息是 event log
2. Configuration over Code
- 用配置表达差异
- 用代码表达共性
- 避免过度抽象
3. AI as State Manager
- 让 AI 管理状态转换
- 不手写复杂状态机
- 适应 LLM 的能力边界
4. Simple, Transparent, Controllable
- 简单胜过复杂
- 透明胜过黑盒
- 用户控制胜过 AI 自动
III. 第一步:消息驱动架构
v2.2.0 - 2025-12-27
问题:双 Source of Truth
最初,研究数据散落在三个地方:
生成报告时,需要从三个地方拼接:
问题:
- 数据不一致:
interviews.conclusion和消息中的访谈内容可能不同步 - 工具调用失败时:数据保存了一半,难以追溯完整上下文
- 扩展困难:添加
discussionChat需要新表、新工具、新查询
更麻烦的是工具输出不一致:
Agent 无法统一处理,导致代码复杂。
方案:Messages as Single Source
核心思路:所有研究内容都输出到消息流,数据库只存派生状态。
关键变化:
-
删除 5 个专用保存工具
- 删除:
saveInterview,saveDiscussion,saveScoutTask, ... - 原因:Agent 直接输出到消息,不需要显式保存
- 删除:
-
统一工具输出格式
- 所有研究工具返回
plainText - Agent 可以统一处理所有工具结果
- 所有研究工具返回
-
按需生成 studyLog
为什么这么设计?
从第一性原理推导:
-
对话即上下文
- LLM 需要完整上下文来生成报告
- 消息历史本身就是最完整、最自然的上下文
- 避免了"从 DB 重建上下文"的复杂性
-
LLM 擅长提取
- 从对话生成结构化内容(studyLog)是 LLM 的强项
- 比手写解析逻辑更灵活、更可靠
-
Event Sourcing 的影子
- 消息序列 = event log
- studyLog、report = 派生状态
- 可以随时重放、重新生成
与其他方案对比:
影响
代码简化:
开发效率:
Before:
After:
成本权衡:
✅ 收益:
- 架构简化:删除 5 个工具,简化 28 个文件
- 数据一致:失败时仍可追溯完整上下文
- 易扩展:添加新研究方式从 12 步 → 3 步
❌ 代价:
- studyLog 生成需要额外 LLM 调用(~2K tokens, ~$0.002)
- 长对话 token 消耗略高
✅ 缓解:
- Prompt cache 将重复 token 成本降低 90%
- 架构收益远大于成本
III. 第二步:意图澄清 + 统一执行
v2.3.0 - 2026-01-06
问题 1:模糊需求 → 低效对话
完成消息驱动架构后,添加新功能变简单了。但用户体验还不够好。
用户创建研究时常说:
"想了解年轻人对咖啡的偏好"
这不够具体:
- 哪个年龄段的年轻人?18-22 的大学生?还是 23-28 的职场新人?
- 用什么方法?深度访谈?群体讨论?还是社交媒体观察?
- 产出什么?用户画像?市场洞察?还是产品建议?
传统做法:AI 多轮追问
问题:
- 需要 3-5 轮对话
- 用户体验差(像在填表)
- AI 无法主动建议最佳方案
问题 2:95% 重复代码
添加功能虽然简化了,但我们发现了更大的技术债:
三个几乎完全相同的 Agent wrapper,总计 1,211 行。
代码重复主要在:
- 消息加载和处理(每个 ~80 行)
- 文件附件处理(每个 ~60 行)
- MCP 集成(每个 ~40 行)
- Token 追踪(每个 ~50 行)
- 通知发送(每个 ~30 行)
每次添加新功能(如 webhook 集成),都要改三个地方。
方案:Plan Mode + baseAgentRequest
我们的解决方案包含两个部分:
Part 1: Plan Mode(意图澄清层)
一个独立的 Agent,专门负责意图澄清:
工作流程:
关键设计:
- Plan Mode 的决策记录在消息中
- Study Agent 从消息推断意图,无需显式传递
- 避免了 context passing 的复杂性
Part 2: baseAgentRequest(统一执行器)
将三个重复的 Agent wrapper 合并成一个通用执行器:
Agent 路由:
每个 Agent 只需定义配置:
为什么这么设计?
推理-执行分离的理由:
-
符合认知模型
- 人类决策:先想清楚"做什么",再考虑"怎么做"
- System 1(直觉)vs System 2(理性)
- Plan Mode = System 2,Study Agent = System 1
-
单一职责
- Plan Mode:专注意图理解,无需知道执行细节
- Study Agent:专注研究执行,无需处理意图澄清
- 每个更简单、更易维护
-
Messages 作为协议
- Plan Mode 的决策 → messages
- Study Agent 从 messages 读取意图
- 松耦合但不失上下文
统一执行器的理由:
-
Extract, Don't Rebuild
- 从三个相似实现中提取公共模式
- 不是从零设计抽象层
-
Configuration over Inheritance
- Agent 差异通过配置表达
- 不用继承或多态
-
Plugin-based Lifecycle
customPrepareStep:动态工具控制customOnStepFinish:自定义后处理- 保留扩展点,不硬编码所有逻辑
与其他方案对比:
影响
代码复杂度:
但更重要的是:
- Cyclomatic Complexity: 12.3 → 6.7(降低 45%)
- 重复代码:95% → 0%
开发效率:
Before:
After:
用户体验:
Before:
After:
意图澄清:3-5 轮对话 → 1 次确认
III. 第三步:持久化记忆
v2.3.0 - 2026-01-08
问题:AI "健忘症"
有了意图澄清和统一架构,研究流程已经很顺畅。但长期使用中,用户反馈了一个问题:
"为什么每次对话,AI 都要重新问我从事什么行业?"
AI 不记得用户。每次对话都像第一次见面:
- "你从事什么行业?"
- "你关注哪些维度?"
- "你的研究目标是什么?"
用户感觉 AI 很"健忘",体验不够个性化。
根本原因:
LLM 是无状态的。每次对话:
虽然我们有历史对话存在 DB,但:
- 跨对话信息丢失:每个研究是独立的 session
- 重要信息淹没:长对话中的关键信息难以提取
- 无持久化记忆:没有"用户是谁"的长期记忆
方案:双层记忆架构
我们需要一个持久化的记忆系统。但如何设计?
受启发于 Anthropic 的 CLAUDE.md 方法:
- 简单的 Markdown 文件
- 用户可查看、可编辑
- 全文加载到 context
我们采用类似方案,但增加了自动更新机制。
数据模型
双层架构:
-
核心记忆(core)
- Markdown 格式,人类可读
- 长期稳定的用户信息
- 例如:
-
工作记忆(working)
- JSON 格式,结构化
- 待整合的新信息
- 例如:
自动更新机制
两阶段更新:
Memory Update Agent(Haiku 4.5):
- 从对话中提取新的用户信息
- 成本低(~$0.001/次)
- 每次对话后台运行
Memory Reorganize Agent(Sonnet 4.5):
- 整合 working memory 到 core memory
- 移除冗余、合并相似信息
- 成本稍高(~$0.02/次),但不频繁触发
集成到对话流程
为什么这么设计?
为什么选择 Markdown 而非 Vector DB?
-
Context Window 够大
- Claude 3.5 Sonnet: 200K tokens
- 用户记忆通常 < 10K 字符(~3K tokens)
- 全文加载比检索更简单、更准确
-
简单透明
- Markdown 用户可读、可编辑
- 无 embedding、无 vector search、无复杂索引
- 符合 Anthropic 的设计哲学:用户可控
-
避免过早优化
- 不需要实时检索(对话频率低)
- 不需要精确匹配(全文提供足够上下文)
- 先做简单方案,必要时再优化
与主流方案对比:
我们选择了 Anthropic 的简单方案,原因:
- 适合当前规模(个人助手,而非企业知识库)
- 用户可控(透明、可编辑)
- 随着 context window 增长,这个方案会更优
影响
用户体验:
Before:
After:
系统成本:
响应时间:
成本低、响应快,完全可接受。
IV. 架构对比:我们的独特选择
现在让我们后退一步,看看 atypica 的架构与主流 AI Agent 框架的区别。
状态管理:Messages vs Memory Classes
为什么不同?
LangChain 的设计受传统软件影响,认为"状态应该显式存储和管理"。
我们认为,对于 LLM:
- 对话历史 = 完整状态
- 派生状态(studyLog)可以重新生成
- 更简单、更容错
Agent 架构:Configuration vs Graph
为什么不同?
LangGraph 追求通用性,用图编排表达任意复杂流程。
我们认为,对于我们的场景:
- 配置驱动更简单:99% 的需求可以用配置满足
- 单一执行器足够:不需要图编排的灵活性
- 越简单越可靠:抽象层越少,问题越容易定位
记忆系统:Markdown vs Vector DB
为什么不同?
Mem0 追求精确检索,用多种数据库混合。
我们认为,对于个人助手:
- 简单方案足够:用户记忆通常 < 10K
- 透明胜过精确:用户可以查看、编辑记忆
- 随 context 增长会更优:未来 1M tokens 时,这个方案会碾压 Vector DB
核心哲学差异
atypica 的选择:
- 简单、透明、可控
- 适应 LLM 的特性(大 context、非确定性)
- 从实际痛点出发,而非追求架构完美
主流框架的选择:
- 精确、复杂、自动
- 移植传统软件工程模式
- 追求通用性和灵活性
谁对谁错?
都没错。只是:
- 我们的场景(个人研究助手)更适合简单方案
- 随着 context window 增长,简单方案会更优
- 用户信任来自透明,而非 AI 魔法
V. 量化影响
三次演进带来的具体影响:
代码复杂度
开发效率
系统性能
成本和性能影响可忽略不计。
用户体验
VI. 经验教训
三次演进中,我们学到了什么?
做对的事
1. 渐进式重构,不做大爆炸
我们没有一次性重写整个系统。三次演进,每一步都:
- 能独立交付价值
- 保持向后兼容(保留
analyst.studySummary字段) - 可以回滚
这让我们能快速验证想法,降低风险。
2. 从真实痛点出发
不追求架构完美,而是:
- 消息驱动:因为添加
discussionChat太复杂 - 统一执行:因为重复代码太多
- 持久化记忆:因为用户反馈 AI 健忘
让问题驱动设计,而非设计驱动问题。
3. 拥抱 LLM 的特性
不把 LLM 当作传统软件:
- 不手写状态机,让 AI 从对话推断状态
- 利用大 context window,而非追求精确检索
- 让 AI 生成 studyLog,而非手写解析
适应 LLM 的能力边界,而非对抗它。
付出的代价
1. 抽象层的学习曲线
baseAgentRequest 需要理解才能修改:
- 6 个 phases 的执行流程
customPrepareStep和customOnStepFinish的时机- 泛型约束和类型推断
但:清晰的接口和文档降低了门槛。
2. 按需生成的成本
studyLog 生成需要 LLM 调用(~$0.002/次)。
但:
- Prompt cache 降低 90% 成本
- 架构收益 >> 小额成本
- 可以接受
3. 简单方案的局限
Markdown 记忆不适合:
- 大规模知识库(> 100K tokens)
- 复杂的关系查询
- 多维度检索
但:
- 对个人助手场景足够
- 未来可以升级到 Vector DB
- 先解决 80% 的问题
意外的收获
1. 类型安全带来的信心
重构时,编译器就能发现 99% 的问题。
2. 配置驱动的灵活性
添加 webhook 集成只需:
所有 Agent 自动获得新能力,无需修改配置。
3. Messages 作为协议的威力
Plan Mode 和 Study Agent 通过 messages 通信:
- 解耦:可以独立修改
- 不失上下文:完整的决策过程都在 messages 中
- 可追溯:出问题时可以重播
这是我们没想到的好处。
VII. 未来方向
三次演进让 atypica 更接近通用智能体。但还有更多要做。
短期(3-6 个月)
1. Skills Library
- 将 tools 进一步模块化
- 用户可组合自己的 Agent
- 类似 GPTs,但更灵活
2. Multi-Agent Collaboration
- 不只是串行执行
- 并行研究、交叉验证
- 类似 AutoGPT,但更可控
长期(1-2 年)
3. 向 GEA 演进
- GEA = General Execution Architecture
- 不只是研究 Agent,而是通用的 AI Agent 执行框架
- 可以运行任意类型的 Agent
4. Self-Improving Agent
- Agent 从过去的执行中学习
- 持续优化 prompts 和策略
- 越用越智能
不变的原则
无论如何演进,我们坚持:
- 简单胜过复杂
- 透明胜过黑盒
- 用户控制胜过 AI 自动
VIII. 结语
构建 AI Agent 系统,不是传统软件工程的简单延伸。
我们需要重新思考:
- 什么是状态?(对话历史)
- 什么是接口?(消息协议)
- 什么是控制流?(AI 推理)
atypica 的三次演进,本质上是三次认知升级:
-
从数据库思维 → 数据流思维
- 不维护显式状态,而是从消息推断状态
-
从代码复用 → 配置驱动
- 不追求完美抽象,而是用配置表达差异
-
从无状态 → 记忆增强
- 不依赖精确检索,而是用简单透明的方式
这些选择可能不是最"先进"的。
但它们是:
- 简单的:容易理解、容易调试
- 透明的:用户知道 AI 在做什么
- 可控的:用户可以干预和调整
- 够用的:解决了 80% 的问题
而这,或许才是构建可靠 AI 系统的关键。