Hello-Agents 第 1 章
Hello-Agents 第 1 章:从普通 LLM 调用到最小 Agent
本系列以 Datawhale 的 Hello-Agents 项目为主线,通过阅读教材、运行代码和修改练习,逐步理解 Agent 的控制流、工具调用、状态、记忆和框架实现。
本文对应教材第 1 章,代码位于
code/chapter1/。
一、这一章要解决什么问题
刚开始接触 Agent 时,很容易把 Agent 理解成“更会聊天的 ChatGPT”。但第 1 章真正要建立的认识是:
Agent 不只是调用一次大语言模型,而是让 LLM 根据当前状态决定行动,再通过工具获取外部结果,并把结果带回下一轮决策的一个程序系统。
普通 LLM 调用大致是:
用户问题 -> LLM -> 最终回答
最小 Agent 则是:
用户问题 -> LLM 决定行动 -> 工具执行 -> Observation
-> 更新上下文 -> LLM 再次决定 -> 最终回答
差异不在于是否使用了 LLM,而在于程序是否建立了一个可以持续运行的“感知—思考—行动—观察”循环。
二、我对 Agent 的第一层理解
教材用“环境、传感器、执行器、自主性”解释智能体。
放到 LLM Agent 中,可以这样对应:
| Agent 概念 | 在 Hello-Agents 示例中的对应物 |
|---|---|
| 用户目标 | 查询天气并推荐景点 |
| 环境 | 天气服务、搜索服务以及它们返回的信息 |
| 传感器 | 用户输入、工具返回的结果 |
| 执行器 | get_weather()、get_attraction() 等工具 |
| 决策中枢 | LLM |
| 状态 | prompt_history 中保存的请求、行动和观察 |
这里的“传感器”和“执行器”不一定是物理设备。在软件 Agent 中,API、数据库、搜索引擎、代码解释器,都可以成为 Agent 与外部环境交互的接口。
三、先运行一个不需要 API 的本地版本
学习时,我先运行了项目中用于理解控制流的本地演示:
code/chapter1/FirstAgentLocalDemo.py
这个文件非常适合入门,因为它不依赖真实 LLM、天气 API 或搜索 API。它使用 FakeLLM 模拟模型,用本地函数模拟工具。
这里有一个必须注意的区别:
FakeLLM不是真正的大语言模型。它只是根据 prompt 中是否出现某些 Observation 文本,返回预先写好的下一步输出。
因此,本地版本的目标不是验证模型能力,而是先把 Agent 程序的控制流跑通。
四、最小 Agent 的完整数据流
下面这张图概括了本地示例的运行链路。

1. 准备系统提示词和工具规则
FirstAgentLocalDemo.py 第 14~22 行定义了 AGENT_SYSTEM_PROMPT。它要求模型每次输出一组 Thought 和 Action,并规定 Action 只能调用天气工具、景点工具或 Finish。
这其实是一份“交互协议”:
Thought: 我下一步要做什么
Action: 我决定调用哪个工具,或者结束任务
如果模型输出格式和程序解析规则不一致,程序就无法知道下一步应该调用哪个工具。因此,提示词不是普通说明文字,它还承担了约束模型输出格式的作用。
2. 第一次构造 Prompt 并请求模型
main() 从第 91 行开始。第 93~94 行创建用户问题和 prompt_history:
user_prompt = "上海今天下雨,帮我推荐适合的室内景点"
prompt_history = [f"用户请求: {user_prompt}"]
这里的 prompt_history 是当前任务的状态。它一开始只有用户请求,后面还会追加模型输出和工具观察结果。
第 99~105 行进入循环,先执行:
full_prompt = "\n".join(prompt_history)
llm_output = llm.generate(full_prompt, system_prompt=AGENT_SYSTEM_PROMPT)
"\n".join(prompt_history) 的含义是:把列表中的多条历史记录,用换行符拼接成一段完整文本。它相当于把之前发生过的事情重新交给模型参考。
第一次调用时,模型只能看到用户问题,所以返回:
Thought: 我需要先知道上海今天的天气,然后才能根据天气推荐室内景点。
Action: get_weather(city="上海")
3. 解析 Action,而不是直接执行整段文本
模型返回的是字符串,程序不能直接把整段字符串当作 Python 函数执行。
parse_action() 位于第 63~67 行。它使用正则表达式寻找 Action: 后面的内容,并返回:
get_weather(city="上海")
随后,run_tool() 位于第 70~88 行,负责把 Action 字符串拆成三部分:
- 工具名:
get_weather - 参数名:
city - 参数值:
上海
第 84~88 行通过 AVAILABLE_TOOLS[tool_name](**kwargs) 真正调用工具。AVAILABLE_TOOLS 在第 57~60 行,是一个“工具名到 Python 函数”的字典:
AVAILABLE_TOOLS = {
"get_weather": get_weather,
"get_attraction": get_attraction,
}
这意味着程序拿到字符串 get_weather 后,可以从字典中找到真正的 get_weather() 函数。
4. 把工具结果写回上下文
工具返回:
上海当前天气:小雨,气温22摄氏度
main() 第 114~116 行把它包装成:
Observation: 上海当前天气:小雨,气温22摄氏度
然后追加到 prompt_history。
这是整个 Agent 循环最关键的地方之一:
工具结果不会自动进入模型上下文,必须由程序显式记录,并在下一轮重新拼接到 Prompt 中。
5. 第二轮根据 Observation 继续决策
下一次循环重新执行:
full_prompt = "\n".join(prompt_history)
此时模型看到的内容已经变成:
用户请求: 上海今天下雨,帮我推荐适合的室内景点
Thought: 我需要先知道上海今天的天气,然后才能根据天气推荐室内景点。
Action: get_weather(city="上海")
Observation: 上海当前天气:小雨,气温22摄氏度
于是它知道天气已经查询完成,下一步可以调用:
Action: get_attraction(city="上海", weather="小雨,22摄氏度")
这说明 Agent 的“状态”并不是模型内部凭空保存的,而是由程序不断更新提示词中的历史记录来实现的。
6. 任务完成时退出循环
当模型认为信息已经足够时,会输出:
Action: Finish[今天上海下雨,推荐去上海博物馆、自然博物馆或中华艺术宫。]
run_tool() 会识别 Finish[...],提取其中的最终答案。main() 第 110~112 行检测到 Finish 后打印结果并 break,Agent 任务结束。
五、真实 API 版本与本地版本有什么不同
本地演示理解的是控制流,教材的 FirstAgentTest.py 则把其中的模拟部分换成了真实服务:
| 部分 | 本地演示 | 真实 API 版本 |
|---|---|---|
| LLM | FakeLLM.generate() | OpenAICompatibleClient.generate() |
| 天气工具 | 返回固定文本的本地函数 | 调用 wttr.in API |
| 景点工具 | 返回固定文本的本地函数 | 调用 Tavily Search API |
| 工具选择 | 由预设字符串决定 | 由真实 LLM 生成 Thought/Action |
| 运行成本 | 不消耗 API 配额 | 需要模型和搜索服务配置 |
真实版本中,OpenAICompatibleClient.generate() 位于 FirstAgentTest.py 第 113~139 行。它把 system prompt 和用户历史封装为 messages,再调用 OpenAI 兼容接口,最后从 response.choices[0].message.content 取得模型文本。
主循环位于第 162 行以后,整体结构仍然没有改变:
构造 full_prompt
-> 调用真实 LLM
-> 解析 Action
-> 调用天气或搜索工具
-> 追加 Observation
-> 继续下一轮
这也是一个很重要的学习结论:接入真实模型并没有改变 Agent 的本质,只是把“模拟决策器”换成了真实 LLM,把“本地工具”换成了真实外部服务。
六、这一章最容易混淆的几个问题
1. Agent 和 LLM 到底有什么区别?
LLM 负责根据输入生成文本;Agent 负责组织一整个任务执行过程。
在本案例中,LLM 只负责回答“下一步应该做什么”。真正的循环、工具执行、结果保存和任务结束,都由 Python 程序完成。
因此可以把 Agent 理解成:
Agent = LLM + Prompt + 状态 + 工具 + 控制循环
2. Thought 是模型内部的真实思维吗?
在这个教学案例中,不应把它理解成模型内部不可见的思维过程。它是模型按照 system prompt 生成的一段文本,用于表达“下一步行动的理由”。程序真正依赖的是它后面的 Action,因为 Python 会解析 Action 并执行工具。
3. 为什么必须循环,而不是一次调用完成?
因为第二个工具依赖第一个工具的结果:只有先知道天气,才能决定推荐什么景点。
所以流程是有数据依赖的:
查询天气 -> 得到天气 Observation -> 查询合适的景点
如果没有工具结果反馈,LLM 就无法基于最新事实继续决策。这正是 Agent Loop 的价值。
4. prompt_history 是记忆吗?
它可以看作最简单的短期任务记忆,但还不是完整的记忆系统。
当前示例只是把本次任务中的用户请求、模型输出和 Observation 保存在列表里。后续章节会进一步讨论消息对象、跨轮对话历史、工具结果以及框架中的记忆管理。
七、我的学习体会与难点
这一章刚开始看起来像是在“照着代码调用 LLM”,真正运行后才发现重点不是 API 写法,而是下面这条控制链:
LLM 给出行动意图
-> Python 解析行动
-> Python 调用工具
-> Python 记录工具结果
-> LLM 根据新结果继续行动
我遇到的主要难点有三个:
- 把 Agent 和“更会聊天的 LLM”区分开。 关键不在模型本身,而在模型外部的循环和工具执行逻辑。
- 理解上下文状态的来源。 状态不是自动存在模型内部,而是通过不断更新
prompt_history,再把完整历史传给下一次 LLM 调用。 - 理解模型输出为什么能触发工具。 模型只是输出约定格式的文本;真正调用 Python 函数的是
parse_action()、run_tool()和AVAILABLE_TOOLS。这是一种“文本协议”,不是模型直接执行代码。
八、本章小结
通过第 1 章,我建立了一个最小 Agent 的工作模型:
- 用户提出目标;
- LLM 根据 Prompt 决定下一步行动;
- Python 程序解析 Action;
- 工具访问外部环境并返回结果;
- 程序把结果写入历史;
- LLM 根据 Observation 继续决策;
- 输出
Finish后结束任务。
后续学习 ReAct、Plan-and-Solve、Reflection,以及 LangGraph 等框架时,都会反复看到这条主线。不同框架的区别,主要在于它们如何组织 Prompt、状态、工具和循环,而不是完全脱离这套基本原理。
更多推荐




所有评论(0)