hello-agents-chapter1-初识智能体

前言

在本案例中,我们将实现一个智能旅行助手Agent,其工作流程如下:

1.接收用户请求:查询某地天气,并根据天气推荐景点;

2.调用天气API(wttr.in API)获取目标城市天气

3.根据天气,调用景点搜索API,推荐合适的景点;(Tavily Search API)

4.全程根据LLM(Large Language Model,大语言模型)决策下一步该做什么,最终返回结果给用户。

基本流程是:用户请求 -> LLM 思考决策 -> 调用工具 -> 获取结果 -> LLM判断是否结束 -> 返回最终答案。

准备工作

1.Python 3.10+
2.API配置
2.1 LLM API配置:使用AIHubmix API

AIHubmix整合了市面上主流的LLM。

2.1.1 访问AIHubmix官方网站:推理时代,首次使用需要注册账户。

2.1.2 访问模型中心:模型 - 推理时代,在筛选条件中选择"免费"标签。建议选择coding-glm-4.7-free,或者其它兼容OpenAI接口的LLM。

2.1.3 获取API凭证:秘钥 - 推理时代,可以使用系统默认的key,也可以自己创建一个key。所谓API KEY,就是一张会员卡。可以将大模型/天气服务/景点推荐服务比作收费的俱乐部,而API KEY就是俱乐部的门禁卡+会员卡。有了API KEY才能调用AI或者其它的服务。

妥善保存以下配置信息,接下来还要使用:

  • API Key: your_api_key

  • Base URL: https://aihubmix.com/v1

  • 推荐模型: coding-glm-4.7-free

2.2 Tavily API配置

Tavily 是一个专为 AI 应用设计的搜索 API,用于景点推荐功能。

2.2.1 访问官网:Tavily

2.2.2 注册账号并获取API密钥。

3.环境配置
3.1 创建虚拟环境
bash 复制代码
# 进入项目目录
cd "hello-agents"
​
# 创建虚拟环境
python -m venv venv
​
# 激活虚拟环境
# Windows:
venv\Scripts\activate
# macOS/Linux:
source venv/bin/activate

虚拟环境下,每个Python项目都有独立的Python解释器和包目录,互不干扰。普通环境下所有Python项目共用一套Python包,如果两个项目需要的包版本不同, 就会出现冲突。此外,普通环境下,Python包都安装在Python系统目录下,时间长了就会臃肿,清理时也容易搞坏系统级别的Python工具。虚拟环境下Python包都存放在venv目录中。因此,建议创建虚拟环境。

3.2 安装依赖包
bash 复制代码
# 安装核心依赖
pip install requests>=2.31.0
pip install tavily-python>=0.3.0
pip install openai>=1.0.0
pip install python-dotenv>=1.0.0
3.3 使用.env文件配置环境变量

3.3.1 在项目根目录创建.env文件

3.3.2 编辑.env文件,添加以下内容

bash 复制代码
# Tavily API 配置
TAVILY_API_KEY=your_tavily_api_key
​
# 大语言模型 API 配置
OPENAI_API_KEY=your_aihubmix_api_key
OPENAI_BASE_URL=https://aihubmix.com/v1
MODEL_NAME=coding-glm-4.7-free

如果你选择的模型不是coding-glm-4.7-free,就填入你选择的模型名称。

配置环境变量的目的在于:我们在代码中不需要通过硬编码的方式写入,即不需要在代码中显式地展示我们的这些配置信息。只需要通过os.getenv读取对应环境变量名的值就可以了。一方面便于管理,另一方面也避免了我们的隐私泄露。.gitignore文件中已经包含了venv和env文件,意味着这两个文件不会被添加到暂存区,那也就不会被上传到Gitee或Github这些代码托管平台了,确保了隐私安全。否则,别人拿到了你的API KEY,就可以拿去疯狂刷额度了。

当然,配置系统环境变量也可以。

3.4 修改代码配置

在code/chapter1/FirstAgentTest.py文件中,找到第143-148行的配置部分:

bash 复制代码
# --- 1. 配置LLM客户端 ---
# 请根据您使用的服务,将这里替换成对应的凭证和地址
API_KEY = "YOUR_API_KEY"
BASE_URL = "YOUR_BASE_URL"
MODEL_ID = "YOUR_MODEL_ID"
os.environ['TAVILY_API_KEY'] = "YOUR_TAVILY_API_KEY"

替换为:

bash 复制代码
import os
from dotenv import load_dotenv
​
load_dotenv()
​
# --- 1. 配置LLM客户端 ---
# 请根据您使用的服务,将这里替换成对应的凭证和地址
API_KEY = os.getenv("OPENAI_API_KEY")
BASE_URL = os.getenv("OPENAI_BASE_URL")
MODEL_ID = os.getenv("MODEL_NAME")
os.environ['TAVILY_API_KEY'] = os.getenv("TAVILY_API_KEY")
3.5 选择虚拟环境路径

以PyCharm为例,Ctrl+Alt+S找到设置(或者在左上角的四条横线那里也能找到) -> 项目:hello-agents -> Python解释器 -> 添加解释器 -> 选择现有 -> 在文件选择窗口中导航到项目目录下(也就是hello-agents目录)的.venv\Scripts\python.exe,选择并确认

3.6 运行项目

3.6.1 进入项目目录(即hello-agents目录),或直接在项目目录下打开cmd

3.6.2 进入虚拟环境:

bash 复制代码
# windows
.\.venv\Scripts\activate.bat

3.6.3 进入.py文件所在目录

bash 复制代码
cd code\chapter1

3.6.4 执行

bash 复制代码
python FirstAgentTest.py

理想情况下的输出内容,请见"实践"部分的末尾。

退出虚拟环境,只需执行deactivate即可。

实践

1.系统提示词
python 复制代码
AGENT_SYSTEM_PROMPT = """
你是一个智能旅行助手。你的任务是分析用户的请求,并使用可用工具一步步地解决问题。

# 可用工具:
- `get_weather(city: str)`: 查询指定城市的实时天气。
- `get_attraction(city: str, weather: str)`: 根据城市和天气搜索推荐的旅游景点。

# 输出格式要求:
你的每次回复必须严格遵循以下格式,包含一对Thought和Action:

Thought: [你的思考过程和下一步计划]
Action: [你要执行的具体行动]

Action的格式必须是以下之一:
1. 调用工具:function_name(arg_name="arg_value")
2. 结束任务:Finish[最终答案]

# 重要提示:
- 每次只输出一对Thought-Action
- Action必须在同一行,不要换行
- 当收集到足够信息可以回答用户问题时,必须使用 Action: Finish[最终答案] 格式结束

请开始吧!
"""

这个提示词就像我们平时在聊天框里与LLM对话一样,我们可以告诉LLM我们希望它做什么,并且阐明我们的要求。在这个系统提示词中,我们为LLM赋予了身份,告诉它能调用哪些工具,并指定了它的输出要求,目的是确保它的输出能被代码解析与执行。

假设我们在聊天框里给LLM发送了这么一段话,那么,LLM怎么知道所谓的可用工具get_weather和get_attraction是什么呢?

2.工具函数1:get_weather(天气查询)
python 复制代码
import requests

def get_weather(city: str) -> str:
    """
    通过调用 wttr.in API 查询真实的天气信息。
    """
    # API端点,我们请求JSON格式的数据
    url = f"https://wttr.in/{city}?format=j1"
    
    try:
        # 发起网络请求
        response = requests.get(url)
        # 检查响应状态码是否为200 (成功)
        response.raise_for_status() 
        # 解析返回的JSON数据 
        data = response.json()
        
        # 提取当前天气状况
        current_condition = data['current_condition'][0]
        weather_desc = current_condition['weatherDesc'][0]['value']
        temp_c = current_condition['temp_C']
        
        # 格式化成自然语言返回
        return f"{city}当前天气:{weather_desc},气温{temp_c}摄氏度"
        
    except requests.exceptions.RequestException as e:
        # 处理网络错误
        return f"错误:查询天气时遇到网络问题 - {e}"
    except (KeyError, IndexError) as e:
        # 处理数据解析错误
        return f"错误:解析天气数据失败,可能是城市名称无效 - {e}"

url后面的那个字符串前面带了个f,表示这是格式化字符串,我们可以往里面放参数。format=j1表示指定返回JSON格式,方便解析。

我们怎么知道data里有这些字段呢?可以直接在浏览器访问https://wttr.in/Beijing?format=j1,就能查看完整的JSON结构,就知道里面有哪些字段了。当然,你可以将Beijing改为你想查询的城市。返回的JSON结构大概是这样:(由于长度过长,只展示部分内容)

python 复制代码
{
  "current_condition": [
    {
      "FeelsLikeC": "15",
      "FeelsLikeF": "59",
      "cloudcover": "0",
      "humidity": "39",
      "lang_zh": [
        {
          "value": "晴朗"
        }
      ],
      "localObsDateTime": "2026-03-27 10:42 PM",
      "observation_time": "02:42 PM",
      "precipInches": "0.0",
      "precipMM": "0.0",
      "pressure": "1016",
      "pressureInches": "30",
      "temp_C": "15",
      "temp_F": "59",
      "uvIndex": "0",
      "visibility": "10",
      "visibilityMiles": "6",
      "weatherCode": "113",
      "weatherDesc": [
        {
          "value": "Clear"
        }
      ],
      "weatherIconUrl": [
        {
          "value": "https://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0008_clear_sky_night.png"
        }
      ],
      "winddir16Point": "SSW",
      "winddirDegree": "201",
      "windspeedKmph": "14",
      "windspeedMiles": "9"
    }
  ],
  ...,
}

可见,data['current_condition'][0]就是上面展示出来的内容,因为data['current_condition']实际上只有一个元素。用变量current_condition接收这个内容,表示当前的天气综合情况。

current_condition['weatherDesc'][0]['value']则是进一步查找信息。定位到weatherDesc处:

python 复制代码
      "weatherDesc": [
        {
          "value": "Clear"
        }
      ],

实际上weatherDesc也只有一个元素,所以current_condition['weatherDesc'][0]就是里面的:

python 复制代码
        {
          "value": "Clear"
        }

再通过['value']得到了"Clear",即current_condition['weatherDesc'][0]['value']的值为"Clear"。用变量weather_desc接收这个内容,表示对当前天气情况的描述。

那么,temp_c = current_condition['temp_C']也是大同小异了。定位到temp_C处:

python 复制代码
      "temp_C": "15",

得到其值为"15",用变量temp_C接受这个内容,表示当前该地区的摄氏度。

得到了这几个基本信息之后,就可以返回语句了:

python 复制代码
        return f"{city}当前天气:{weather_desc},气温{temp_c}摄氏度"

当然,如果你想要再给LLM多一些信息,也可以照葫芦画瓢,获取这个JSON字符串的更多字段的值。

3.工具函数2:get_attraction(景点推荐)
python 复制代码
import os
from tavily import TavilyClient

def get_attraction(city: str, weather: str) -> str:
    """
    根据城市和天气,使用Tavily Search API搜索并返回优化后的景点推荐。
    """

    # 从环境变量或主程序配置中获取API密钥
    api_key = os.environ.get("TAVILY_API_KEY") # 推荐方式
    # 或者,我们可以在主循环中传入,如此处代码所示

    if not api_key:
        return "错误:未配置TAVILY_API_KEY。"

    # 2. 初始化Tavily客户端
    tavily = TavilyClient(api_key=api_key)
    
    # 3. 构造一个精确的查询
    query = f"'{city}' 在'{weather}'天气下最值得去的旅游景点推荐及理由"
    
    try:
        # 4. 调用API,include_answer=True会返回一个综合性的回答
        response = tavily.search(query=query, search_depth="basic", include_answer=True)
        
        # 5. Tavily返回的结果已经非常干净,可以直接使用
        # response['answer'] 是一个基于所有搜索结果的总结性回答
        if response.get("answer"):
            return response["answer"]
        
        # 如果没有综合性回答,则格式化原始结果
        formatted_results = []
        for result in response.get("results", []):
            formatted_results.append(f"- {result['title']}: {result['content']}")
        
        if not formatted_results:
             return "抱歉,没有找到相关的旅游景点推荐。"

        return "根据搜索,为您找到以下信息:\n" + "\n".join(formatted_results)

    except Exception as e:
        return f"错误:执行Tavily搜索时出现问题 - {e}"

我们通过API_KEY初始化Tavily客户端。Tavily是一个轻量级搜索API,因此我们可以搜索该城市在某种天气下推荐的景点,即可以根据城市+天气进行景点推荐。我们将city和weather这两个传入的参数放入格式化字符串当中,作为查询语句query的一部分。在tavily.search()方法中,将query传入。此外,search_depth表示搜索级别,"basic"表示基础搜索,这使得搜索速度相对快一些、额度也足够。include_answer=true表示让API直接返回总结好的回答,不需要我们自己拼接搜索结果。search方法的使用可以在Tavily官网的API Playground查看。

例如,查询广州在晴朗天气下推荐的景点:

返回的结果是一个JSON字符串,由于其内容太长,这里就不展示了。但是从上面的图片中我们可以看到,这个JSON字符串有一个字段为answer,也就是API所做的总结性回答:

复制代码
"answer": "In clear weather, visit Guangzhou Tower for panoramic views, explore Chenjia Ci for traditional architecture, and enjoy Longfeng Park for family fun.",

这也是为什么我们在查询后会先尝试response.get("answer"),我们并不是事先就知道了这个JSON字符串有这个字段的,是通过查看官方网站的使用案例知道的。

另外,这里还有降级处理。如果没有总结回答,就拼接原始结果返回,保证有输出:

python 复制代码
        formatted_results = []
        for result in response.get("results", []):
            formatted_results.append(f"- {result['title']}: {result['content']}")

answer是最精炼的回答,result则是存放字典的数组,里面有具体的信息。for循环得到了results里面的每一个字典,然后再将字典中key为title和content的value拼接起来,组成了一个相对没那么精炼的结果。

4.工具注册表
python 复制代码
available_tools = {
    "get_weather": get_weather,
    "get_attraction": get_attraction,
}

将所有工具函数和名称映射起来,后续解析LLM的"Action"时,能快速找到对应的函数执行。

5.LLM客户端
python 复制代码
from openai import OpenAI

class OpenAICompatibleClient:
    """
    一个用于调用任何兼容OpenAI接口的LLM服务的客户端。
    """
    def __init__(self, model: str, api_key: str, base_url: str):
        self.model = model
        self.client = OpenAI(api_key=api_key, base_url=base_url)

    def generate(self, prompt: str, system_prompt: str) -> str:
        """调用LLM API来生成回应。"""
        print("正在调用大语言模型...")
        try:
            messages = [
                {'role': 'system', 'content': system_prompt},
                {'role': 'user', 'content': prompt}
            ]
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
                stream=False
            )
            answer = response.choices[0].message.content
            print("大语言模型响应成功。")
            return answer
        except Exception as e:
            print(f"调用LLM API时发生错误: {e}")
            return "错误:调用语言模型服务时出错。"

在init方法中,我们传入模型名称,传入api_key和base_url以构建一个OpenAI客户端。实际上,兼容OpenAI接口的模型都能使用这个客户端,例如GPT、国产大模型。就相当于你买了一台iPhone,会给你送原装的苹果充电线,这就是OpenAI接口与GPT的关系。而其他人的iPhone也能使用你这条充电线,因为它们都兼容苹果充电线的接口,这就是其它大模型(兼容OpenAI接口)与OpenAI接口的关系。

在generate方法中,我们构造了对话信息(系统提示词+用户请求),调用LLM的API,返回模型的回答。

具体怎么操作,可以在Github上找到OpenAI的open-ai-python仓库,里面有说明。举两个例子:

The primary API for interacting with OpenAI models is the Responses API. You can generate text from the model with the code below.

中文:与 OpenAI 模型进行交互的主要 API 是响应 API。您可以使用以下代码从模型生成文本。

python 复制代码
import os
from openai import OpenAI

client = OpenAI(
    # This is the default and can be omitted
    # 注:omitted 可省略的
    api_key=os.environ.get("OPENAI_API_KEY"),
)

response = client.responses.create(
    model="gpt-5.2",
    instructions="You are a coding assistant that talks like a pirate.",
    input="How do I check if a Python object is an instance of a class?",
)

print(response.output_text)

The previous standard (supported indefinitely) for generating text is the Chat Completions API. You can use that API to generate text from the model with the code below.

中文:之前用于生成文本的标准(无限期支持)是 Chat Completions API。您可以使用下面的代码通过该 API 从模型生成文本。

python 复制代码
from openai import OpenAI

client = OpenAI()

completion = client.chat.completions.create(
    model="gpt-5.2",
    messages=[
        {"role": "developer", "content": "Talk like a pirate."},
        {
            "role": "user",
            "content": "How do I check if a Python object is an instance of a class?",
        },
    ],
)

print(completion.choices[0].message.content)

可以看到,上面这个例子很贴合我们项目中的代码:

python 复制代码
            messages = [
                {'role': 'system', 'content': system_prompt},
                {'role': 'user', 'content': prompt}
            ]
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
                stream=False
            )
            answer = response.choices[0].message.content
            print("大语言模型响应成功。")
            return answer

只不过我们事先将messages写在了外面,在messages当中,我们告诉LLM这轮对话中有两个角色,并给定了这两个角色的提示词。再调用client.chat.completions.create方法获得来自LLM的响应reponse,返回即可。

6.主循环:核心逻辑,协调LLM与工具的交互
python 复制代码
import re
import os
from dotenv import load_dotenv

load_dotenv()

# --- 1. 配置LLM客户端 ---
# 请根据您使用的服务,将这里替换成对应的凭证和地址
API_KEY = os.getenv("OPENAI_API_KEY")
BASE_URL = os.getenv("OPENAI_BASE_URL")
MODEL_ID = os.getenv("MODEL_NAME")
os.environ['TAVILY_API_KEY'] = os.getenv("TAVILY_API_KEY")

llm = OpenAICompatibleClient(
    model=MODEL_ID,
    api_key=API_KEY,
    base_url=BASE_URL
)

# --- 2. 初始化 ---
user_prompt = "你好,请帮我查询一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。"
prompt_history = [f"用户请求: {user_prompt}"]

print(f"用户输入: {user_prompt}\n" + "="*40)

# --- 3. 运行主循环 ---
for i in range(5): # 设置最大循环次数
    print(f"--- 循环 {i+1} ---\n")
    
    # 3.1. 构建Prompt
    full_prompt = "\n".join(prompt_history)
    
    # 3.2. 调用LLM进行思考
    llm_output = llm.generate(full_prompt, system_prompt=AGENT_SYSTEM_PROMPT)
    # 模型可能会输出多余的Thought-Action,需要截断
    match = re.search(r'(Thought:.*?Action:.*?)(?=\n\s*(?:Thought:|Action:|Observation:)|\Z)', llm_output, re.DOTALL)
    if match:
        truncated = match.group(1).strip()
        if truncated != llm_output.strip():
            llm_output = truncated
            print("已截断多余的 Thought-Action 对")
    print(f"模型输出:\n{llm_output}\n")
    prompt_history.append(llm_output)
    
    # 3.3. 解析并执行行动
    action_match = re.search(r"Action: (.*)", llm_output, re.DOTALL)
    if not action_match:
        observation = "错误: 未能解析到 Action 字段。请确保你的回复严格遵循 'Thought: ... Action: ...' 的格式。"
        observation_str = f"Observation: {observation}"
        print(f"{observation_str}\n" + "="*40)
        prompt_history.append(observation_str)
        continue
    action_str = action_match.group(1).strip()

    if action_str.startswith("Finish"):
        final_answer = re.match(r"Finish\[(.*)\]", action_str).group(1)
        print(f"任务完成,最终答案: {final_answer}")
        break
    
    tool_name = re.search(r"(\w+)\(", action_str).group(1)
    args_str = re.search(r"\((.*)\)", action_str).group(1)
    kwargs = dict(re.findall(r'(\w+)="([^"]*)"', args_str))

    if tool_name in available_tools:
        observation = available_tools[tool_name](**kwargs)
    else:
        observation = f"错误:未定义的工具 '{tool_name}'"

    # 3.4. 记录观察结果
    observation_str = f"Observation: {observation}"
    print(f"{observation_str}\n" + "="*40)
    prompt_history.append(observation_str)

流程如下:

1.使用前面写好的OpenAICompatibleClient类,配置LLM客户端

2.写入我们的提示词user_prompt,并创建一个prompt_history数组,每次循环都把LLM的输出与工具输出封装起来,放到prompt_history数组中,让LLM记住自己做过什么。例如,已经查询了天气,就要进行景点推荐了。

3.进入循环。在循环中,我们每次都要将之前的prompt_history加入到新的提示词当中,因为LLM是无状态的,它并不能记住之前做过什么。然后调用前面我们写好的generate方法。在generate方法中,我们真正去调用LLM,并返回了LLM响应的结果,这里我们使用llm_output进行接收。

4.截断LLM的输出。为了防止LLM话太多,乱输出,我们只截取第一对有效的Thought+Action,后面全部丢掉。

python 复制代码
    match = re.search(r'(Thought:.*?Action:.*?)(?=\n\s*(?:Thought:|Action:|Observation:)|\Z)', llm_output, re.DOTALL)

这个正则表达式的意思是:我要找到一段文字,满足:

  • 以"Thought:"开头,

  • 中间包含任意内容

  • 直到遇到"Action:"为止

  • 后面紧接着换行+"Thought:"、换行+"Action:"、换行+"Observation:",或者直接到文本末尾

再通过if match判断是否找到了满足上述四个要求的文字,如果找到了,就执行截断逻辑:

python 复制代码
        truncated = match.group(1).strip()
        if truncated != llm_output.strip():
            llm_output = truncated
            print("已截断多余的 Thought-Action 对")

match.group表示拿到匹配到的的第一段内容,strip()表示删除前后多余的空格与换行,再用truncated接收这个纯净的文本。

如果这个纯净的文本和LLM输出的内容一样,说明LLM的输出很干净,因此不需要进行截断。如果不一样,就需要进行截断,进入if循环,把纯净的文本赋给llm_output即可。

最后,输出这个纯净的文本,并将其放入到prompt_history中,以便后续LLM使用:

python 复制代码
    print(f"模型输出:\n{llm_output}\n")
    prompt_history.append(llm_output)

虽然我们对LLM下过规定,要求它一次输出一对Thought+Action,但它经常会不听话,可能一次输出2对、3对,可能输出完Action之后又输出Observation、可能自己跟自己对话。

因为我们的Agent的模式是:LLM输出第一次思考 -> 程序执行工具 -> 返回结果 -> 再进行下一次思考,所以要对LLM的输出进行截断。

LLM的输出是一个黑盒,因为大模型并不是程序,只是概率生成文本的机器。它不会严格遵守格式,只会尽量模仿格式。其内部是巨大的神经网络,输入的文字变成向量,中间经过几千层计算,输出下一个字的概率分布,中间发生了什么是无法追踪的。因此,我们要用代码的确定性去约束LLM的不确定性。

5.解析LLM的输出,并执行行动

5.1 先将"Action:"后面的内容从llm_output中提取出来:

python 复制代码
action_match = re.search(r"Action: (.*)", llm_output, re.DOTALL)

5.2 如果没找到Action,报错并重试:

python 复制代码
    if not action_match:
        observation = "错误: 未能解析到 Action 字段。请确保你的回复严格遵循 'Thought: ... Action: ...' 的格式。"
        observation_str = f"Observation: {observation}"
        print(f"{observation_str}\n" + "="*40)
        prompt_history.append(observation_str)
        continue

我们会将报错信息放到历史提示词当中,也就是prompt_history当中,然后回到循环开头,就会再次调用generate方法,真正调用LLM,这样LLM也会读取到这个错误信息。

5.3 再将action的内容拿出来

python 复制代码
   action_str = action_match.group(1).strip()

re.search返回的结果只是一个盒子,将里面的内容拿出来还需要调用group方法。

5.4 判断任务是否结束:在AGENT_SYSTEM_PROMPT中,我们规定:

python 复制代码
Action的格式必须是以下之一:
1. 调用工具:function_name(arg_name="arg_value")
2. 结束任务:Finish[最终答案]

如果action的内容是调用工具,意味着没结束。如果action的是"Finish[]",意味着结束了。

5.4.1 如果任务结束了

python 复制代码
    if action_str.startswith("Finish"):
        final_answer = re.search(r"Finish\[(.*)\]", action_str, re.DOTALL).group(1))
        print(f"任务完成,最终答案: {final_answer}")
        break

如果action_str是以"Finish"开头的,意味着任务结束了。

在AGENT_SYSTEM_PROMPT里,我们规定:

python 复制代码
当收集到足够信息可以回答用户问题时,必须使用 Action: Finish[最终答案] 格式结束

因此,将"Finish[]"里中括号的内容解析出来,用final_answer接收,返回final_answer即可。

解释:

  • re.DOTALL = 允许内容跨越多行

  • re.search = 不要求必须从开头匹配(原来的re.match只匹配开头)

5.4.2 如果任务没有结束,说明在action中LLM告诉了我们它想调用的工具,因此我们要将其解析出来,然后真的去调用这个函数,并把结果返回给LLM。

假设现在:

python 复制代码
action_str = 'get_weather(city="北京")'
    tool_name = re.search(r"(\w+)\(", action_str).group(1)

表示找"("左边的单词,相当于拿到了工具名。在上面的例子中,相当于tool_name="get_weather"。

python 复制代码
   args_str = re.search(r"\((.*)\)", action_str).group(1)

表示将"()"中间的内容抽取出来,相当于提取参数。在上面的例子中,相当于args_str='city="北京"'。

python 复制代码
    kwargs = dict(re.findall(r'(\w+)="([^"]*)"', args_str))

把key="value"格式变为Python字典,结果:

python 复制代码
kwargs = { "city": "北京" }

接着,真正去调用这个工具:

python 复制代码
    if tool_name in available_tools:
        observation = available_tools[tool_name](**kwargs)
    else:
        observation = f"错误:未定义的工具 '{tool_name}'"

如果这个工具在我们前面的工具注册表中存在,就调用它。available_tools是工具注册表,是一个字典,available_tools[tool_name]则是找到key为tool_name的value,这个value实际上是函数引用。拿到函数引用之后传参即可。翻译成人话就是:调用 get_weather 函数,并且传入参数 city="北京"。

也就是说,这里才是Agent真正做事的地方。

5.5 由于我们使用observation接收工具执行后的结果,因此要将这个结果输出,并加入到prompt_history当中,返回给LLM,以便LLM读取:

python 复制代码
    # 3.4. 记录观察结果
    observation_str = f"Observation: {observation}"
    print(f"{observation_str}\n" + "="*40)
    prompt_history.append(observation_str)

这样,在下一轮循环中,LLM又能看到这次的结果,继续思考。

可见,LLM只会输出文字,而Agent就是用LLM决策,用代码去执行。

总结一下解析的流程:

复制代码
循环开始 → 拼接历史上下文 → 调用LLM得到Thought-Action → 解析Action → 
  ├─ 如果是Finish → 结束任务,返回答案
  └─ 如果是调用工具 → 执行工具 → 记录结果 → 回到循环开头,让LLM继续思考

运行结果:

复制代码
用户输入: 你好,请帮我查询一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。
========================================
--- 循环 1 ---

正在调用大语言模型...
大语言模型响应成功。
模型输出:
Thought: 用户要求查询北京今天的天气,并根据天气推荐旅游景点。我需要先查询北京的天气情况。

Action: get_weather(city="北京")

Observation: 北京当前天气:Sunny,气温19摄氏度
========================================
--- 循环 2 ---

正在调用大语言模型...
大语言模型响应成功。
模型输出:
Thought: 我已经查询到北京今天的天气是晴天,气温19摄氏度。现在我需要根据这个天气情况为用户推荐合适的旅游景点。

Action: get_attraction(city="北京", weather="Sunny")

Observation: On sunny days in Beijing, visit the Summer Palace for its beautiful gardens and historic architecture. The Temple of Heaven offers a serene atmosphere and cultural significance. The Forbidden City is also worth visiting for its historical importance.
========================================
--- 循环 3 ---

正在调用大语言模型...
大语言模型响应成功。
模型输出:
Thought: 我已经获得了北京今天的天气情况和适合的旅游景点推荐。信息已经足够完整,可以给用户一个完整的回答了。

Action: Finish[今天北京的天气是晴天,气温19摄氏度。根据这个天气情况,为您推荐以下旅游景点:

1. 颐和园 - 晴天非常适合欣赏颐和园美丽的园林和壮观的历史建筑
2. 天坛 - 可以在宁静的氛围中感受文化意义
3. 故宫 - 值得参观,具有重要的历史价值

这些都是晴天游览北京的好选择,祝您旅途愉快!]

任务完成,最终答案: 今天北京的天气是晴天,气温19摄氏度。根据这个天气情况,为您推荐以下旅游景点:

1. 颐和园 - 晴天非常适合欣赏颐和园美丽的园林和壮观的历史建筑
2. 天坛 - 可以在宁静的氛围中感受文化意义
3. 故宫 - 值得参观,具有重要的历史价值

这些都是晴天游览北京的好选择,祝您旅途愉快!

提交PR

我们发现了先前代码的错误,并修改为如下结果:

python 复制代码
final_answer = re.search(r"Finish\[(.*)\]", action_str, re.DOTALL).group(1))

因此,我们要提交PR,步骤如下:

1.在Github中的原hello-agents仓库,点击fork

fork之后相当于我们自己创建了一个内容一样的仓库。

2.给本地项目添加自己的远程仓库

2.1 打开git bash,进入hello-agents项目目录

2.2 执行:

bash 复制代码
git remote add myfork https://github.com/你的GitHub用户名/hello-agents.git

2.3 查看是否成功

bash 复制代码
git remote -v

你会看到origin是原作者,myfork则是自己的仓库。

3.提交修改

执行:

bash 复制代码
git add .
git commit -m "fix:修复chapter01中正则表达式匹配换行报错问题"
4.推送到自己的仓库

执行:

bash 复制代码
git push myfork main

如果报错:

! [rejected] main -> main (fetch first) error: failed to push some refs to 'https://github.com/KevinJosephDavis/hello-agents.git' hint: Updates were rejected because the remote contains work that you do not hint: have locally. This is usually caused by another repository pushing to hint: the same ref. If you want to integrate the remote changes, use hint: 'git pull' before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.

原因是:远程 Fork 仓库(GitHub 上)和本地不一致,Git 不让你直接推。

执行:

bash 复制代码
git fetch myfork
git rebase myfork/main

这两步没问题后,执行:

bash 复制代码
git push myfork main

如果还是报错,强制推送:

bash 复制代码
git push -f myfork main
5.写PR

自己的仓库中,点击contribute -> open pull request

标题:fix: fix regex crash in chapter01 when content contains newlines 修复chapter01中正则表达式匹配换行报错问题

内容:Fixed the AttributeError caused by re.match() not supporting multi-line content.

Changes:

  • Replace re.match() with re.search()

  • Add re.DOTALL flag to support multi-line matching

  • Resolve 'NoneType' object has no attribute 'group' error

点击Create pull Requests即可。

思考与总结

我们使用豆包时,有时会发现AI在做出回答之前,会先显示"已搜索到xx条内容",再返回搜索结果。这实际上和我们在本章所做的智能旅行助手agent一样。在我们的案例中,虽然最核心的内容是调用LLM,但为了规范LLM的行为,我们告诉给它两个现成的工具get_weather和get_attraction,并指定它的输出格式,又因为LLM并不能做到完全规范,我们还要对它的输出进行处理。对于我们平时与Gemini、Ds、豆包、GPT进行聊天的聊天框,约等于一个超级增强版的、已封装好的Agent(不是指聊天框本身这个界面,指的是调用背后的逻辑)。我们看到它显示搜索到多少条内容,实际上是LLM正在调用搜索工具API,例如我们本次案例中的获取某地天气的API、根据天气推荐景点的API。实际上,之前我在使用DS的时候,有一次它给我返回的内容结构都是JSON字符串,且有Thought和Action字段,后面我让它不要显示这些内容,它才进行了改正。这也说明,其内部也有思考->行动这个流程。

如果我们不使用LLM,就需要自己先获取某地天气,再查询对应网站获取根据天气推荐景点的结果,有了LLM,它就会根据我们告诉它有哪些工具,自己做决策并调用对应的工具。例如我们在系统提示词中告诉了LLM,它能使用get_weather和get_attraction这两个工具,但是这两个工具的具体执行,则是由我们自己实现的。

实际上这个过程就类似于人的行动。假设我们现在要完成查询天气 -> 根据天气推荐景点这个任务,流程一般如下:

:我先要查天气

:使用现成工具:天气 APP 进行查询

看结果:北京晴天 25℃

再想:晴天适合去户外景点(基于上一步的结果:晴天)

再做:搜索景点

再看结果:推荐颐和园

最后想:信息够了,可以回答

总结起来,就是思考 -> 行动 -> 观察 -> 重复 -> 结束。而Agent也是在效仿这个流程:代码里的循环就是在做这件事:

  1. 循环开始

  2. LLM 思考(Thought)

  3. LLM 决定行动(Action)

  4. 使用工具

  5. 得到结果(Observation)

  6. 把结果放回历史

  7. 回到循环顶部,继续下一步

LLM就好比人脑,人脑是无法感知外界环境的,因此我们需要嘴、眼、鼻、耳等工具,这些工具会将外界反馈的味道、场景、气味、声音(对应工具返回的结果)交给大脑(LLM),大脑会根据这些信息做出决策。例如在《轻音少女》中,律能分辨mio的脚步声,工作流程如下:

1.大脑保持思考

2.调用听觉(工具),获取脚步声

3.将脚步声交给大脑(LLM)

4.大脑思考

5.大脑调用记忆(工具),查找脚步声(其实这些数据在大脑中的存在形式也类似于键值对)

6.返回键值对,其中key为"mio的脚步声",value为刚刚听见的脚步声

7.大脑得到结果:这是mio的脚步声

8.大脑思考:mio来做什么呢?

9.调用推理(工具),做出判断

10.将推理结果返回给大脑

11.大脑得出结论:mio是来看望我的

如果我们将一个人的各个感知器官都关闭,那么大脑的输出显然会乱掉,因为这个人已经完全无法感知外界。例如将你的眼睛蒙上,耳朵与鼻子堵上,然后带你去外面开了100公里的车再回到你家,你大概率是不知道现在到了你家,你的大脑只会根据它仅有的工具(触觉等)做出判断,然后这时你的大脑就会疯狂思考"我现在在哪里",当你很绝望的时候,你的大脑可能会乱下结论:这里是监狱。

通过上面这个比喻,我们也能明白为什么要遵循思考 -> 行动 -> 观察 -> 重复 -> 结束的流程。因为LLM本身是无法感知外部环境的,且LLM的输出是黑盒,你让它一次想五步(蒙上你的眼带你开2小时的车),有可能会格式错乱、返回错误结果。且LLM推理长度越长(将2小时延长),LLM的错误率越高(你做出错误决策的可能性就越大)。

相关推荐
li三河2 小时前
opencv利用freetype写中文
人工智能·opencv·计算机视觉
2601_955363152 小时前
技术赋能B端拓客:号码核验的行业困局与破局路径氪迹科技法人股东筛选系统,阶梯式价格
大数据·人工智能
lpfasd1232 小时前
2026年第13周GitHub趋势周报:AI Agent与多模态工具链加速融合
人工智能
zandy10112 小时前
告别指标混乱:衡石科技指标管理平台的AI自治之路
人工智能·科技
亓才孓2 小时前
【提示词五要素】
python·ai·prompt
财经资讯数据_灵砚智能2 小时前
全球财经资讯日报(夜间-次晨)2026年3月28日
大数据·人工智能·python·语言模型·ai编程
bingyu98752 小时前
OpenClaw 在 WSL 中的完整安装与配置指南
ai·openclaw
爱听歌的周童鞋2 小时前
Learn-Claude-Code | 笔记 | Tools & Execution | s01 The Agent Loop | s02 Tools
llm·agent·note·claude code·tool use·agent loop
非优秀程序员3 小时前
10分钟,用qclaw打造你的AI选股系统--官方每天送4百万token
人工智能·微信·产品经理