跳转到主内容
Logo

从零搭一个最小 Agent:看清一个 Agent 的全部骨架

Published on
Authors

从零搭一个最小 Agent:看清一个 Agent 的全部骨架

这篇笔记记录我从一个空文件夹开始,手写一个最小 agent 的过程。目标不是一上来使用复杂框架,而是先看清楚 agent 最核心的结构:模型如何看到工具,如何决定调用工具,程序如何执行工具,再把结果交回模型生成最终回答。

最后我也会把这个小 agent 和更复杂的 OpenClaw、Hermes、Claude Code、Codex 做一个对比,帮助理解它们到底复杂在哪里。

1. 从空文件夹开始

练习目录是:

/Users/huashan/Agent-development

一开始我只需要几个文件:

Agent-development/
├── agent.py
├── requirements.txt
├── README.md
├── prompts/
│   └── identity.md
├── memory/
│   └── user_profile.md
└── docs/
    └── agent-learning-note.md

requirements.txt 里只放一个依赖:

anthropic

因为这次我选择走 Anthropic-style 的 tool-use 写法,同时用 MiniMax 的 Anthropic-compatible API 来跑模型。

正文插图 1B

2. 最小 Agent 的核心结构

这个 agent 的最小内核其实只有三样东西:

system prompt + tools + agent loop

在当前项目里分别对应:

prompts/identity.md        # 身份、表达风格、回答规则
memory/user_profile.md     # 对用户的稳定认知
agent.py / TOOLS            # 工具函数和工具 schema
agent.py / run_anthropic_agent()  # agent loop

这里最重要的是理解 agent loop:

用户输入
-> 模型看到 system prompt + user profile + tools schema
-> 模型决定直接回答,还是发出 tool_use
-> Python 收到 tool_use
-> call_tool() 找到对应工具函数
-> Python 执行工具
-> 工具结果包装成 tool_result
-> tool_result 发回模型
-> 模型继续调用工具,或者生成最终回答

这个 loop 就是 agent 的心脏。后面再加身份、记忆、skills、MCP、多 agent,本质上都是围绕这个 loop 扩展。

3. 第一个工具:复合调用,模型自己决定流程

最早的工具集很简单:

calculate(expression)
save_note(text)
read_notes()
get_weather_forecast(location, day="tomorrow")

现在我让模型一次性用上它们:

python3 agent.py --provider minimax "查一下上海今天的天气,把温度范围和是否需要带伞保存到笔记里"

模型没有自己硬算,也不是 Python 提前决定流程。MiniMax-M3 看到这串指令后,自己拆解出三步:

tool_use: get_weather_forecast({"location": "Shanghai", "day": "today"})
tool_use: save_note({"text": "上海 2026-07-02 阴,19-29℃,不带伞"})
tool_use: save_note({"text": "..."})  # 第二条,记录体感提醒

Python 依次执行工具,把 JSON 结果回给模型。模型看到这些结构化数据后,再合成最终回答:

上海今天阴,气温 19-29℃,降水概率 16%,不用带伞,出门带件薄外套就行。
已保存到笔记:天气摘要 + 出行提醒两条。

这个例子虽小,但它已经具备 agent 的关键特征:模型负责拆解任务和决定步骤,程序负责执行工具,工具负责连接外部能力。

4. 工具和 Schema 是两件事

agent.py 里,每个工具都有两面。

第一面是真正执行动作的 Python 函数:

def get_weather_forecast(location: str, day: str = "tomorrow") -> dict[str, Any]:
    ...

第二面是给模型看的 schema:

"get_weather_forecast": Tool(
    name="get_weather_forecast",
    description="Get a daily weather forecast...",
    input_schema={
        "type": "object",
        "properties": {
            "location": {"type": "string"},
            "day": {"type": "string"},
        },
        "required": ["location"],
    },
    function=get_weather_forecast,
)

模型看不到 Python 函数内部怎么写,它只看到 namedescriptioninput_schema。所以 schema 写得清楚,模型才知道什么时候该调用这个工具,以及参数应该怎么填。

这里我用的 Tool(name=..., description=..., input_schema=..., function=...)Anthropic SDK 风格(anthropic.types.Tool)。如果你在用 OpenAI 风格,schema 是裸 {"type":"function","function":{...}} 的字典列表,喂给 client.chat.completions.create(tools=...);核心区别只是包了一层还是裸传,模型看到的字段其实是一样的。

5. 加一个真实能力:天气和带伞建议

接下来我加了一个天气工具:

get_weather_forecast(location, day="tomorrow")

它调用 Open-Meteo API,返回结构化 JSON:

{
  "location": "Shanghai, Shanghai Municipality, China",
  "date": "2026-06-26",
  "condition": "overcast",
  "temperature_min_c": 19.8,
  "temperature_max_c": 28.7,
  "precipitation_sum_mm": 0.0,
  "precipitation_probability_max_percent": 16,
  "should_bring_umbrella": false,
  "umbrella_recommendation": "Umbrella is probably not necessary.",
  "source": "Open-Meteo"
}

然后我问:

python3 agent.py --provider minimax "明天上海的天气如何,你建议我带伞出门吗?"

MiniMax-M3 先调用天气工具,拿到 JSON 后再合成最终回答:

明天上海阴天,气温 19.8-28.7℃,降水概率仅 16%,无明显降雨。

不建议带伞。天气阴但基本不会下雨,出门带件薄外套即可。

这里有一个重要分工:

Python 工具负责拿到可靠数据
LLM 负责理解数据并用人的语言回答

你可以把这一步看成「模型自己合成『出门带件薄外套即可』——它把 precipitation_probability_max_percent=16 翻译成『基本不会下雨』,把 temperature_min_c=19.8 转成『带件薄外套』,这才是 LLM 真正在干活的地方。Open-Meteo 给的是结构化数字,模型把它们折叠成可执行的出行建议。」

这也是 agent 和普通脚本的差别。普通脚本通常是固定流程;agent 会根据模型判断决定是否调用工具、怎么重试、怎么组织最后的回答。

6. Prompt 控制表达风格,不需要加工具

我想让回答更像中文生活助理,于是没有加工具,而是改了身份 prompt。

现在身份放在:

prompts/identity.md

里面有类似这样的规则:

默认用中文回答
先给结论,再给依据
尽量短,通常控制在 5 行以内
对天气问题,必须明确说"建议带伞"或"不建议带伞"

这里可以总结一个判断方法:

改变说话风格 -> 改 prompt
新增外部能力 -> 加 tool

比如"回答要温柔一点""先给结论""用中文"都属于 prompt;"查天气""读文件""发消息""查日历"才属于 tool。

7. 加身份和用户画像

后来我把原来写死在 agent.py 里的 SYSTEM_PROMPT 拆出来,变成两个文件:

prompts/identity.md
memory/user_profile.md

identity.md 负责描述 agent 是谁、怎么说话、有什么基本规则。

user_profile.md 负责描述它对用户的稳定认知,比如:

- 用户正在从零学习如何开发 agent。
- 用户希望用中文交流,喜欢先理解原理,再逐步动手实现。
- 用户当前在练习 Anthropic-style tool-use agent,并使用 MiniMax-M3 运行。

agent.py 里用 build_system_prompt() 把它们拼起来:

def build_system_prompt() -> str:
    identity = (PROMPTS_DIR / "identity.md").read_text(encoding="utf-8")
    user_profile = (MEMORY_DIR / "user_profile.md").read_text(encoding="utf-8")
    runtime_rules = (
        "## Runtime Rules\n"
        "- 当前时间: ${now}\n"
        "- 工作目录: ${cwd}\n"
        "- 如果用户问事实类问题,优先调用工具,不要编造。"
    ).format(now=datetime.now().isoformat(timespec="seconds"), cwd=os.getcwd())

    return f"{identity}\n\n{user_profile}\n\n{runtime_rules}"

拼出来的最终 prompt 结构是:

system prompt = identity + user profile + runtime rules

这样一来,agent 就开始有一点"连续性"了。它不只是每次临时回答,而是带着一个身份和对用户的认知来回答——而且每次启动都知道现在是几点、自己在哪个目录,跨次运行的"上下文"被显式灌进 prompt 里。

正文插图 2B

8. .env:把 API Key 放到本地

一开始每次运行都要写:

export MINIMAX_API_KEY="..."

后来我加了一个很小的 .env 读取器。现在可以在项目根目录放:

MINIMAX_API_KEY=your_key_here

.env 已经加入 .gitignore,不会被提交到 Git。agent.py 启动时会读取它,但如果 shell 里已经 export 了同名变量,会优先使用 shell 的值。最简实现:

from pathlib import Path
import os

def load_dotenv(path: str = ".env") -> None:
    if not Path(path).exists():
        return
    for line in Path(path).read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        os.environ.setdefault(k.strip(), v.strip())  # setdefault: shell 优先

load_dotenv()
api_key = os.environ["MINIMAX_API_KEY"]
正文插图 3B

关键就是 setdefault——已存在的环境变量不会被 .env 覆盖,所以 shell 里临时 export 的值永远赢,本地文件只兜底。

9. 小 Agent 和复杂 Agent 的关系

现在这个 agent 可以概括为:

identity + user_profile + tools + loop

这已经是 agent 的最小骨架。OpenClaw、Hermes、Claude Code、Codex 看起来复杂很多,但它们底层依然离不开这个结构。

差别主要是复杂系统在这个骨架上叠了更多层:

更多入口
更多记忆
更多工具
更多身份
更多权限控制
更强运行时工程
更复杂的任务调度

10. OpenClaw 和 Hermes 复杂在哪里

OpenClaw / Hermes 更像长期运行的个人 AI 系统,而不是一次性命令行 agent。

它们复杂在:

  • 入口更多:Discord、Telegram、语音、cron、webhook、定时任务。
  • 记忆更多:长期记忆、当天记忆、会话历史、向量检索、跨 agent 共享记忆。
  • 工具更多:通知、日历、邮件、文件、搜索、MCP、数据库、交易、语音。
  • agent 更多:主 agent、生活 agent、英语 agent、交易 agent、研究 agent。
  • 运行时更重:常驻服务、日志、监控、重启、权限、密钥、失败告警。

如果说我这个小项目是:

一次性命令行 agent

那 OpenClaw / Hermes 更像:

长期运行的个人 AI 操作系统

但它们仍然离不开同一个循环:

模型看上下文和工具 schema
-> 决定调用工具
-> 程序执行工具
-> 工具结果回给模型
-> 模型继续决策或最终回答

11. Claude Code 和 Codex 算不算 Agent

Claude Code 和 Codex 当然也算 agent,而且是 coding agent。

它们不是生活助理,而是专门住在代码仓库里的软件工程 agent。

普通生活 agent 的 loop 是:

用户问题 -> 判断工具 -> 调工具 -> 回答

coding agent 的 loop 是:

读代码 -> 搜索文件 -> 理解需求 -> 修改代码 -> 跑测试 -> 看报错 -> 再修改 -> 总结结果

本质仍然是:

模型负责决策
工具负责执行
loop 负责闭环

只是工具换成了软件工程工具:

read_file / search_code / apply_patch / run_command / run_tests / git_diff / inspect_logs

Claude Code 和 Codex 复杂在它们要理解真实代码仓库、遵守工程约束、处理 Git 状态、运行测试、避免破坏用户未提交的改动,还要在失败时继续调试。

12. 一张对比表

系统类型核心能力复杂点
本文的小 agent学习用命令行 agenttool-use loop、天气工具、身份、用户画像简单、单进程、无长期运行
OpenClaw / Hermes个人 AI 运行时/多 agent 系统多入口、多 agent、cron、记忆、工具生态会话、权限、插件、运行时稳定性
Claude Codecoding agent读写代码、执行命令、修 bug、跑测试代码理解、仓库上下文、工程安全
Codexcoding agent代码修改、调试、审查、测试闭环工具权限、上下文管理、协作协议

13. 我现在理解 Agent 的方式

经过这个练习,我觉得 agent 可以这样理解:

Agent = LLM + 上下文 + 工具 + 循环 + 状态

其中:

身份 -> prompt
对用户的认知 -> user_profile / memory
外部能力 -> tools
专项任务说明 -> skills
外部工具生态 -> MCP
长期运行能力 -> runtime
多个角色协作 -> multi-agent

所以学习 agent 不一定要一上来就用复杂框架。更好的路线是:

先写一个最小 loop
再加一个真实工具
再加身份
再加用户画像
再加记忆
再加 skills
最后再接 MCP 和多 agent

这样每一层复杂度都能看见,不会被框架一下子淹没。

参考资料