AI Agent 最小可运行项目结构
如果说:
解决的是:
单个 Agent 在代码层,核心骨架应该长什么样
那么这篇文档要解决的就是另一个非 常实际的问题:
当你准备把这些单文件示例真正落成一个最小可运行项目时,目录应该怎么组织?
很多人在学 AI Agent 时,单文件代码看懂了,也能跟着敲出来。
但一准备做成真实项目,就会马上遇到这些问题:
agent.ts、tools.ts、state.ts到底要不要拆scripts放哪,tests放哪,日志放哪- 第一版要不要先上
src/agents、src/core、src/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
负责:
- 项目依赖
- 启动脚本
- 测试脚本
- 开发命令
第一版只要能支持下面几类命令就够了:
devrun:minimalrun:toolrun:researchtest
不要一开始就塞很多脚本别名。
src/index.ts
负责:
- 作为默认程序入口
- 串起配置加载、Agent 初始化和一次运行流程
如果你已经把真正的运行入口放到 scripts/,那 src/index.ts 也可以只是导出公共能力,而不是承担所有启动逻辑。
第一版两种都可以,但建议职责单一。
src/config.ts
负责:
- 读取环境变量
- 解析最小配置
- 给模型、超时、最大步数提供统一入口
第一版不要把配置散落在多个文件里。
哪怕只有:
MODEL_NAMEMAX_STEPSLOG_LEVELAPI_KEY
也值得单独放进一个 config.ts。
src/types.ts
负责:
- 放共享类型
- 统一
AgentState、ToolDefinition、ToolResult之类的基础结构
如果类型非常少,也可以暂时不拆。
但只要你开始同时维护 Minimal Agent、Tool-Using Agent、Research Agent 三种版本,共享类型就值得有一个固定位置。
src/agent/
负责:
- 放不同 Agent 版本的核心循环
- 保持“一个文件对应一种 Agent 主流程”
推荐第一版先这样理解:
minimal-agent.ts:只有最小 loop,重点是Decide -> Act -> Observe -> Stoptool-using-agent.ts:加入更明确的工具选择、调用记录、失败处理research-agent.ts:加入研究维 度、证据、信息缺口和收束逻辑
这里不要急着做:
base-agent.tsabstract-agent.tsagent-factory.tsagent-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:放核心 loopscripts/run.ts:放本地运行入口
这一步的目标只是:
让主逻辑和演示脚本分开。
第二步:把工具拆出去
当你开始有两个以上工具时,就值得把工具移到 src/tools/。
先拆这两层就够了:
- 工具定义/注册
- 工具实现
不要一开始再拆成:
schemas/adapters/providers/executors/
除非你已经真的有复杂度。