跳到主要内容

AI Agent 最小可运行项目结构

如果说:

解决的是:

单个 Agent 在代码层,核心骨架应该长什么样

那么这篇文档要解决的就是另一个非常实际的问题:

当你准备把这些单文件示例真正落成一个最小可运行项目时,目录应该怎么组织?

很多人在学 AI Agent 时,单文件代码看懂了,也能跟着敲出来。

但一准备做成真实项目,就会马上遇到这些问题:

  • agent.tstools.tsstate.ts 到底要不要拆
  • scripts 放哪,tests 放哪,日志放哪
  • 第一版要不要先上 src/agentssrc/coresrc/runtime 这种大结构
  • Tool-Using Agent 和 Research Agent 是不是一开始就要做很多层目录
  • 什么时候该继续拆模块,什么时候反而会把项目拆乱

所以这篇文档不会讲“大而全项目模板”,而是只回答一件事:

给你一个足够小、但真的能跑起来的 AI Agent 最小目录结构。

目标不是让第一版看起来“像大厂项目”,而是让它:

  • 能运行
  • 能调试
  • 能加工具
  • 能写最小测试
  • 能继续演进

1. 为什么需要目录结构,而不只是单文件

单文件示例非常适合学习,因为它有两个优点:

  • 所有逻辑放在一起,容易顺着读
  • 初学时不会被工程结构分散注意力

但它也有很明显的边界。

一旦你真的开始跑一个最小项目,通常很快就会出现这些变化:

  • 你想把环境变量和配置独立出来
  • 你想把工具实现和 Agent loop 分开
  • 你想加一个脚本跑 demo,另一个脚本跑测试
  • 你想保留最基本的日志和运行记录
  • 你想在不改主文件的情况下,继续加新工具或新 Agent 变体

这时候如果还把所有东西塞在一个文件里,问题就会变成:

  • 主文件越来越长
  • 工具定义和工具实现混在一起
  • 测试很难写
  • 不同 Agent 版本会互相污染
  • 一改就容易牵动整份文件

所以目录结构的意义不是“形式专业”,而是:

让第一版项目在保持简单的前提下,开始具备可运行、可维护、可扩展的最小边界。

2. 一个最小可运行项目,推荐先长这样

如果你现在就是要把前面三篇代码文真正变成一个可以跑的最小项目,我更推荐从下面这个结构开始:

minimal-ai-agent/
├─ package.json
├─ tsconfig.json
├─ .env.example
├─ README.md
├─ src/
│ ├─ index.ts
│ ├─ config.ts
│ ├─ types.ts
│ ├─ agent/
│ │ ├─ minimal-agent.ts
│ │ ├─ tool-using-agent.ts
│ │ └─ research-agent.ts
│ ├─ tools/
│ │ ├─ index.ts
│ │ ├─ search-docs.ts
│ │ ├─ read-doc.ts
│ │ └─ fetch-records.ts
│ ├─ prompts/
│ │ ├─ planner.ts
│ │ └─ researcher.ts
│ ├─ state/
│ │ └─ memory-state.ts
│ └─ logging/
│ └─ logger.ts
├─ scripts/
│ ├─ run-minimal.ts
│ ├─ run-tool-agent.ts
│ └─ run-research-agent.ts
├─ tests/
│ ├─ minimal-agent.test.ts
│ ├─ tool-using-agent.test.ts
│ └─ research-agent.test.ts
└─ logs/
└─ .gitkeep

这棵树的重点不是“拆得多漂亮”,而是它满足了第一版最核心的几件事:

  • src/ 里放正式运行代码
  • scripts/ 里放可直接执行的入口
  • tests/ 里放最小验证
  • logs/ 里放运行期输出
  • 三类 Agent 可以共存,但还没有过度抽象

如果你只做一个最小 Agent,甚至可以更小一点:

minimal-ai-agent/
├─ package.json
├─ .env.example
├─ src/
│ ├─ index.ts
│ ├─ agent.ts
│ ├─ tools.ts
│ ├─ state.ts
│ └─ logger.ts
├─ scripts/
│ └─ run.ts
└─ tests/
└─ agent.test.ts

这个版本同样是成立的。

真正重要的是:

先把“能跑”和“能继续加东西”这两个目标同时满足,而不是一开始就设计成框架。

3. 每个目录和文件,第一版分别负责什么

下面这套职责划分,基本够你把三篇代码文都落成项目。

package.json

负责:

  • 项目依赖
  • 启动脚本
  • 测试脚本
  • 开发命令

第一版只要能支持下面几类命令就够了:

  • dev
  • run:minimal
  • run:tool
  • run:research
  • test

不要一开始就塞很多脚本别名。

src/index.ts

负责:

  • 作为默认程序入口
  • 串起配置加载、Agent 初始化和一次运行流程

如果你已经把真正的运行入口放到 scripts/,那 src/index.ts 也可以只是导出公共能力,而不是承担所有启动逻辑。

第一版两种都可以,但建议职责单一。

src/config.ts

负责:

  • 读取环境变量
  • 解析最小配置
  • 给模型、超时、最大步数提供统一入口

第一版不要把配置散落在多个文件里。

哪怕只有:

  • MODEL_NAME
  • MAX_STEPS
  • LOG_LEVEL
  • API_KEY

也值得单独放进一个 config.ts

src/types.ts

负责:

  • 放共享类型
  • 统一 AgentStateToolDefinitionToolResult 之类的基础结构

如果类型非常少,也可以暂时不拆。

但只要你开始同时维护 Minimal AgentTool-Using AgentResearch Agent 三种版本,共享类型就值得有一个固定位置。

src/agent/

负责:

  • 放不同 Agent 版本的核心循环
  • 保持“一个文件对应一种 Agent 主流程”

推荐第一版先这样理解:

  • minimal-agent.ts:只有最小 loop,重点是 Decide -> Act -> Observe -> Stop
  • tool-using-agent.ts:加入更明确的工具选择、调用记录、失败处理
  • research-agent.ts:加入研究维度、证据、信息缺口和收束逻辑

这里不要急着做:

  • base-agent.ts
  • abstract-agent.ts
  • agent-factory.ts
  • agent-runtime-manager.ts

这些东西不是不能做,而是第一版通常还没有足够稳定的共性。

src/tools/

负责:

  • 放工具定义和实现
  • 每个工具尽量一个文件
  • index.ts 统一导出工具注册表

第一版最稳的规则是:

一个工具文件,只负责一种清晰能力。

比如:

  • search-docs.ts:负责搜索候选文档
  • read-doc.ts:负责读取某篇文档内容
  • fetch-records.ts:负责取某类业务记录

不要一开始就写一个“全能工具文件”。

src/prompts/

负责:

  • 放 planner prompt
  • 放 research prompt
  • 把提示词从 Agent 主逻辑里拿出来

如果你的第一版 prompt 还非常短,放在 Agent 文件里也能跑。

但只要你开始调整:

  • 决策规则
  • 工具选择说明
  • 研究维度模板
  • 最终总结模板

把 prompt 拆出来就会明显更清楚。

src/state/

负责:

  • 管理运行期状态
  • 放最小 memory 结构
  • 处理状态初始化和更新

第一版通常不需要数据库,也不需要持久化。

所以这里最推荐的是:

  • 先做内存态 memory-state.ts
  • 只负责创建、读取、更新一次运行中的状态

第一版不要急着引入:

  • Redis
  • 向量数据库
  • 长期记忆层
  • 跨会话状态同步

src/logging/

负责:

  • 统一日志输出
  • 记录步骤、工具调用、错误和最终结果摘要

第一版最小要求其实很低:

  • 能打印到控制台
  • 能选择性写到 logs/
  • 字段不要太乱

只要你能稳定看到:

  • 当前 step
  • 调了哪个 tool
  • tool 成功还是失败
  • 最后为什么结束

就已经足够支持第一轮调试。

scripts/

负责:

  • 放“人直接执行”的脚本
  • 区分不同 Agent 的运行入口
  • 不把演示代码塞进 src/ 主逻辑

这是一个很容易被忽略、但非常实用的目录。

因为第一版项目通常会有很多“带一点临时性质的运行入口”:

  • 本地跑最小 demo
  • 手动触发某个 Tool-Using Agent
  • 跑一次 research task

这些都比放进 src/index.ts 更适合放在 scripts/

tests/

负责:

  • 放最小自动化验证
  • 先测状态流转和工具调用,不追求大而全

第一版不要把测试目标定得过高。

最值得先测的是:

  • 最大步数是否会停止
  • 工具失败时状态是否正确更新
  • Research Agent 遇到信息缺口时是否继续而不是直接结束
  • 最终输出是否至少包含必要字段

logs/

负责:

  • 存放运行日志或调试输出
  • 和源码目录分开

第一版这个目录甚至可以只有一个 .gitkeep

重点不是日志系统多高级,而是:

别把运行生成物混回源码目录。

4. 从单文件版本迁移到目录化版本,最稳的顺序是什么

很多人从教程代码迁移到项目结构时,最容易犯的错误是:

一次性重构太多。

更稳的方式是按下面这个顺序来。

第一步:先把“运行入口”和“核心逻辑”分开

如果你现在只有一个 agent-demo.ts,先不要急着拆很多目录。

先做最小切分:

  • src/agent.ts:放核心 loop
  • scripts/run.ts:放本地运行入口

这一步的目标只是:

让主逻辑和演示脚本分开。

第二步:把工具拆出去

当你开始有两个以上工具时,就值得把工具移到 src/tools/

先拆这两层就够了:

  • 工具定义/注册
  • 工具实现

不要一开始再拆成:

  • schemas/
  • adapters/
  • providers/
  • executors/

除非你已经真的有复杂度。

第三步:把状态和配置拆出去

当你发现这些内容开始在多个文件里重复出现时:

  • AgentState
  • maxSteps
  • modelName
  • 环境变量读取

就把它们分别收到:

  • src/state/
  • src/config.ts
  • src/types.ts

这一步的目标不是“分层漂亮”,而是:

避免每个 Agent 文件都重复维护同样的底层定义。

第四步:再决定要不要拆 prompt 和 logging

只有当你真的出现下面这些情况时,再拆:

  • prompt 已经很长
  • 不同 Agent 用不同决策模板
  • 日志字段开始变复杂
  • 你需要统一记录运行轨迹

也就是说,prompts/logging/ 是很常见的第二轮拆分,而不是第一分钟就必须存在。

5. 最小的 scripts、tests、logging,应该各自放哪里

这个问题看起来小,但其实决定了第一版项目会不会乱。

最推荐的放法是:

  • 可执行脚本放 scripts/
  • 自动化测试放 tests/
  • 日志实现放 src/logging/
  • 日志输出文件放 logs/

可以简单理解成:

scripts/ -> 负责“怎么跑”
tests/ -> 负责“怎么验”
src/logging/ -> 负责“怎么记”
logs/ -> 负责“记下来的东西放哪”

这个边界非常重要。

因为第一版项目一旦把这些东西混在一起,后面通常就会出现:

  • 运行入口散在源码里
  • 临时调试文件混进 src/
  • 测试和 demo 共用一堆隐式状态
  • 日志文件跑到项目根目录到处都是

目录结构的价值,很多时候不是“增加抽象”,而是:

减少混放。

6. 从最小 Agent,到 Tool-Using Agent,再到 Research Agent,目录怎么演进

这三类 Agent 不需要一开始就分成完全不同的项目。

更推荐的演进方式是:

阶段一:最小 Agent

这时你的重点只是把最小 loop 跑起来。

目录可以非常克制:

src/
├─ agent.ts
├─ tools.ts
├─ state.ts
└─ logger.ts

这时先回答的是:

  • 会不会无限循环
  • 有没有基本状态
  • 能不能完成一次最小任务

阶段二:Tool-Using Agent

当工具开始变多,状态也更丰富时,再升级为:

src/
├─ agent/
│ ├─ minimal-agent.ts
│ └─ tool-using-agent.ts
├─ tools/
│ ├─ index.ts
│ ├─ search-docs.ts
│ ├─ read-doc.ts
│ └─ fetch-records.ts
├─ state/
│ └─ memory-state.ts
├─ config.ts
└─ types.ts

这时你主要是在解决:

  • 工具职责拆分
  • 工具注册
  • 调用历史记录
  • 基本失败处理

阶段三:Research Agent

当任务开始强调研究维度、证据和信息缺口时,再继续加:

src/
├─ agent/
│ ├─ minimal-agent.ts
│ ├─ tool-using-agent.ts
│ └─ research-agent.ts
├─ prompts/
│ ├─ planner.ts
│ └─ researcher.ts
├─ tools/
│ ├─ index.ts
│ ├─ search-docs.ts
│ ├─ read-doc.ts
│ └─ rank-sources.ts
├─ state/
│ └─ memory-state.ts
├─ logging/
│ └─ logger.ts
├─ config.ts
└─ types.ts

这时你解决的是:

  • 检索和阅读分层
  • 证据结构化
  • 信息缺口显式记录
  • 研究过程可回看

你会发现,这个演进路径的核心不是“越拆越多”,而是:

每次只为新复杂度增加一层必要结构。

7. 什么时候再拆模块,什么时候先不要拆

很多人最难把握的,其实不是“怎么拆”,而是:

什么时候才值得拆。

一个很实用的判断方式是:

可以考虑继续拆的时候

通常你已经出现下面这些信号:

  • 同一文件超过 200 到 300 行,而且明显在做两件以上的事
  • 工具数量已经增加到 4 到 6 个以上
  • 你开始维护两种以上 Agent 变体
  • prompt 已经长到影响主流程阅读
  • 测试时需要频繁 mock 某些公共模块
  • 日志、状态、工具调用已经形成稳定模式

这时拆模块会让项目更清楚。

先不要继续拆的时候

如果你现在只是:

  • 只有 1 到 2 个工具
  • 只有一个最小 Agent
  • 还在频繁改状态结构
  • prompt 还很短
  • 还没形成稳定的复用边界

那就先别急着拆。

因为这时候过早抽象,常见后果是:

  • 文件数上去了,但真实边界并不清楚
  • 很多目录只有一个文件,且职责并不稳定
  • 一开始写了很多 basemanagerfactory
  • 后面真正迭代时,反而要把这些抽象拆掉重来

所以更稳的原则是:

先让重复出现,再为重复拆结构。

8. 第一版不要急着加什么

如果你的目标只是“把这三篇代码文变成一个最小可运行项目”,第一版非常不建议急着加下面这些东西:

  • 不要急着上数据库
  • 不要急着做长期记忆
  • 不要急着做消息队列
  • 不要急着做多 Agent 编排
  • 不要急着做复杂插件系统
  • 不要急着做配置中心
  • 不要急着做可视化工作流编排
  • 不要急着做很重的抽象基类体系

这些东西不是没用。

而是它们通常都属于:

第二阶段甚至更后面的复杂度。

第一版更值得优先保证的是:

  • Agent loop 真的能稳定结束
  • 工具调用轨迹是清楚的
  • 状态更新是可理解的
  • 研究任务能形成最小闭环
  • 目录结构不会妨碍后续增加新工具和新 Agent 变体

只要这些成立,第一版就已经是一个好的起点。

9. 一个适合起步的最小原则

如果你现在正准备把单文件示例变成真实项目,可以先记住这个最小原则:

先按运行边界拆,不要按想象中的最终架构拆。

也就是说,优先拆的是:

  • 入口
  • Agent 主循环
  • 工具
  • 状态
  • 配置
  • 测试
  • 日志

而不是一开始就拆:

  • 平台层
  • 领域层
  • 编排层
  • 插件层
  • 多租户层
  • 长期记忆层

因为对于一个最小可运行 Agent 项目来说,真正重要的不是“结构看起来大”,而是:

目录能准确反映当前系统的真实复杂度。

10. 小结:把最小项目搭起来,比把结构想满更重要

这篇文档的核心建议可以压缩成三句话:

  • 单文件示例适合学习,但最小项目需要最基本的目录边界
  • 第一版目录结构要服务于运行、调试、测试和继续演进
  • 不要为了“像框架”而过早拆分,先为真实复杂度留出位置

如果你要把前面的三篇代码文真正落成一个项目,第一版最推荐的思路就是:

  • 先做一个能跑的最小目录树
  • 先把 scripts / tests / logging 放到清楚的位置
  • 再从 Minimal Agent 平滑演进到 Tool-Using AgentResearch Agent

这样你得到的不是一个“看起来完整”的空架子,而是一个:

真的能开始写、开始跑、开始改的最小 AI Agent 项目。