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 字符串拆成三部分:

  1. 工具名:get_weather
  2. 参数名:city
  3. 参数值:上海

第 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 版本
LLMFakeLLM.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 根据新结果继续行动

我遇到的主要难点有三个:

  1. 把 Agent 和“更会聊天的 LLM”区分开。 关键不在模型本身,而在模型外部的循环和工具执行逻辑。
  2. 理解上下文状态的来源。 状态不是自动存在模型内部,而是通过不断更新 prompt_history,再把完整历史传给下一次 LLM 调用。
  3. 理解模型输出为什么能触发工具。 模型只是输出约定格式的文本;真正调用 Python 函数的是 parse_action()、run_tool() 和 AVAILABLE_TOOLS。这是一种“文本协议”,不是模型直接执行代码。

八、本章小结

通过第 1 章,我建立了一个最小 Agent 的工作模型:

  1. 用户提出目标;
  2. LLM 根据 Prompt 决定下一步行动;
  3. Python 程序解析 Action;
  4. 工具访问外部环境并返回结果;
  5. 程序把结果写入历史;
  6. LLM 根据 Observation 继续决策;
  7. 输出 Finish 后结束任务。

后续学习 ReAct、Plan-and-Solve、Reflection,以及 LangGraph 等框架时,都会反复看到这条主线。不同框架的区别,主要在于它们如何组织 Prompt、状态、工具和循环,而不是完全脱离这套基本原理。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐