DeepSeek + ReAct 实现 Agent

  1. 大模型自身的局限 ===========

最近 DeepSeek 的爆火,再一次点燃了大家对于大模型的热情,身边很多日常不关注 AI 领域的人,也开始交流 DeepSeek 的使用体验。 DeepSeek 的确非常强大,它将推理能力拉齐到了 GPT 水平的同时,还大幅降低了训练成本,成为当下最炙手可热的大模型

但是,即使像 DS 如此强大的大模型,它也不是万能的,有一些问题它也回答不了,特别是一些垂直领域的知识或实时性比较高的知识。比如我们想做一个居家生活小助手,直接调用 DeepSeek 是行不通的(未开启联网搜索的情况下)。

picture.image

那么如何解决这个问题呢? 每当我们遇到这种需要模型做自主判断、自行调用工具、自行决定下一步行动的时候,Agent 就轮到出场了。

  1. Agent 概述 ===========

后面有机会我们会详细讲解下 AI Agent,这里先简单介绍一下: Agent 是一种基于 LLM 驱动的、能够自主感知周围环境、做出决策、采取行动达成特定目标的系统。一个 Agent 系统的整体架构如下:

picture.image

一个 Agent 通常包含了以下几个关键模块:

  • 规划(Planning):它负责将大目标分解成小的子目标,也可以对已有行为进行反思和自我改善。
  • 记忆(Memory):包括短期记忆和长期记忆,短期记忆提供上下文内的学习,长期记忆则提供长时间保留和回忆信息的能力。
  • 工具(Tools):通过调用外部 API 获取外部信息(作为感知器),执行外部动作(作为执行器)。

Agent 看起来是一个非常复杂的系统,但是实际上有一种快速的实现方式,那就是 LLM + ReAct 框架 。那么什么是 ReAct 呢?

  1. ReAct 核心原理 =============

ReAct 实际上是两个单词的缩写:Reasoning + Acting,也就是推理 + 行动,它是一个引导大语言模型进行推理和行动的思维框架。在《ReAct: Synergizing Reasoning and Acting in Language Models》这篇论文中首次提出。

我们引用论文中的示例来解释一下。假设我们想要问大模型这样一个问题:

除了苹果遥控器,还有哪些设备可以控制苹果遥控器最初设计用来交互的程序?Aside from the Apple Remote, what other devices can control the program Apple Remote was originally designed to interact with?

使用 ReAct 框架,可以引导大模型进行如下的推理:

picture.image

在这个例子中,大模型为了完成一个复杂任务时,首先会进行子任务拆分,且每个子任务的执行都会经过如下几个阶段:

  • Thought 思考:大模型根据任务进行思考和推理,制定执行计划。
  • Action 行动:大模型从可用的工具列表中筛选出可用的工具,执行具体的动作。
  • Observation 观察:动作执行完成后,由大模型观察执行结果,并判断是继续下一步动作,还是执行结束返回结果。

理论好像大概了解了,但具体要如何实现呢? 下面我们就基于 DeepSeek 大模型 + ReAct 框架,实现一个简单的 Agent,解决文章开头提出的那个查询水果价格的问题。

  1. ReAct Agent 实战 =================

申请 api_key

一步一步来。首先,我们需要搞定 DeepSeek 的 api_key。

最近 DeepSeek 实在太火了,由于算力和资源的限制,DeepSeek 的官方平台可能没有那么稳定,我们可以考虑使用大厂的云平台,比如阿里云百炼,使用阿里云私有部署的 DeepSeek 模型。具体的流程就不展开了,大家按照官网的说明操作即可。

picture.image

定义 Tools 工具

工具本质上就是我们为大模型提供的扩展能力,它们可以是一些 Open API(如 Google 搜索、高德天气等等),也可以是我们内部的一些函数,甚至是第三方的服务。

这里为了方便演示,我们实现一个本地的函数 query\_fruit\_unit\_price 作为工具, 它以 Mock 的方式查询水果的价格。

另外,我们也针对该工具编写标准的调用参数:

  
# -*- coding: utf-8 -*-  
"""  
@Time    : 2025/3/4 20:35   
@Author  : ZhangShenao   
@File    : tools.py   
@Desc    : 工具模块  
"""  
  
  
def query_fruit_unit_price(fruit_name: str) -> str:  
    """  
    查询水果单价  
    :param fruit_name: 水果名称  
    :return: 水果单价  
    """  
    if fruit_name == "苹果":  
        return "2.8"  
    if fruit_name == "香蕉":  
        return "1.6"  
    return "未查询到该种类水果的价格"  
  
  
# 可以调用的外部工具描述  
TOOLS_DESCRIPTION = [  
    {  
        "name": "query_fruit_unit_price",  
        "description": "使用该工具可以查询到指定种类水果的单价",  
        "parameters": {  
            "type": "object",  
            "properties": {  
                "fruit_name": {  
                    "type": "string",  
                    "description": "水果名称",  
                }  
            },  
            "required": ["fruit_name"]  
        },  
    },  
]  

构造 Promot

ReAct 的 Prompt 比较复杂,但是我们并不需要自己探索,因为 LangChain 官方的 Prompt Hub 中已经提供了一个 Prompt 模版,个人认为这是一段可以封神的 Promopt,我们直接拿来用就可以了:

  
{instructions}  
  
TOOLS:  
------  
  
You have access to the following tools:  
  
{tools}  
  
To use a tool, please use the following format:  
  

Thought: Do I need to use a tool? Yes
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action

  
When you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:  
  

Thought: Do I need to use a tool? No
Final Answer: [your response here]

  
Begin!  
  
Previous conversation history:  
{chat_history}  
  
New input: {input}  
{agent_scratchpad}

这段 Prompt 中主要包含下面这些关键内容:

  • instructions:类似 System Prompt,是为大模型设置的指令和人设。

  • tools:定义工具列表和描述信息,告诉大模型有哪些工具是可以使用的,并且具体的用法是什么。

  • chat_history:聊天历史,即对话的上下文信息。

  • agent_scratchpad:这是一个 Agent 剪贴板,用于记录 Agent 的思考过程。这部分是可选的,并不影响整个 Agent 的执行过程。

对这段 Prompt 进行格式化,就可以生成一个完整的 ReAct Prompt 了。

  
# -*- coding: utf-8 -*-  
"""  
@Time    : 2025/3/4 20:37   
@Author  : ZhangShenao   
@File    : prompt.py   
@Desc    : prompt提示词模块  
"""  
from typing import List, Dict  
  
# ReAct Prompt模板  
REACT_PROMPT_TEMPLATE = """  
{instructions}  
  
TOOLS:  
------  
  
You have access to the following tools:  
  
{tools}  
  
To use a tool, please use the following format:  
  

Thought: Do I need to use a tool? Yes
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action

  
Then wait for Human will response to you the result of action by use Observation.  
... (this Thought/Action/Action Input/Observation can repeat N times)  
When you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:  
  

Thought: Do I need to use a tool? No
Final Answer: [your response here]

  
Begin!  
  
New input: {input}  
  
"""  
  
  
def build_react_prompt(instructions: str, query: str, tool_desc: List[Dict], tool_name: str) -> str:  
    """  
    构造ReAct Prompt  
    :param instructions: 系统指令  
    :param query: 用户的提问  
    :param tool_desc: 外部工具描述  
    :param tool_name: 外部工具名称  
    :return: React Prompt  
    """  
  
    return REACT_PROMPT_TEMPLATE.format(instructions=instructions,  
                                        tools=tool_desc,  
                                        tool_names=tool_name,  
                                        input=query)  

封装 LLM

接下来,我们对大模型进行一些简单的封装,便于后面的调用。

  
# -*- coding: utf-8 -*-  
"""  
@Time    : 2025/3/4 20:45   
@Author  : ZhangShenao   
@File    : llm.py   
@Desc    : LLM大模型模块  
"""  
import os  
from typing import List, Dict  
  
import dotenv  
from openai import OpenAI, Stream  
from openai.types.chat import ChatCompletion, ChatCompletionChunk  
  
  
class LLM:  
    def __init__(self, model_name: str):  
        """  
        初始化LLM大模型  
        :param model_name: LLM大模型名称  
        """  
  
        # 加载环境变量  
        dotenv.load_dotenv()  
  
        # 创建通义百炼客户端,兼容OpenAI协议  
        self._client = OpenAI(  
            api_key=os.getenv("DASHSCOPE_API_KEY"),  
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"  
        )  
        self._model_name = model_name  
  
    def send_msg(self, messages: List[Dict]) -> ChatCompletion | Stream[ChatCompletionChunk]:  
        """  
        发送消息  
        :param messages: 消息列表  
        :return: 消息发送结果  
        """  
  
        return self._client.chat.completions.create(  
            model=self._model_name,  
            messages=messages,  
        )  

执行 Agent

  
最后一部分就是执行 Agent 了。
  
# -*- coding: utf-8 -*-  
"""  
@Time    : 2025/3/4 20:45   
@Author  : ZhangShenao   
@File    : agent.py   
@Desc    : Agent模块  
"""  
import json  
import re  
  
from llm import LLM  
from prompt import build_react_prompt  
from tools import TOOLS_DESCRIPTION, query_fruit_unit_price  
  
if __name__ == '__main__':  
    # 构造Prompt  
    instructions = "你是一个居家生活小助手,可以回答用户的日常问题。"  
    query = "我想买2个苹果和3根香蕉,一共需要多少钱?"  
    prompt = build_react_prompt(instructions=instructions,  
                                query=query,  
                                tool_desc=TOOLS_DESCRIPTION,  
                                tool_name="query_fruit_unit_price",  
                                )  
  
    # 创建LLM  
    llm = LLM(model_name="deepseek-v3")  
  
    # 保存上下文  
    print(f"初始提问: {prompt}")  
    messages = [{"role": "user", "content": prompt}]  
  
    # 执行ReAct过程  
    while True:  
        response = llm.send_msg(messages)  
        response_text = response.choices[0].message.content  
  
        print(f"大模型的回复:\n{response_text}")  
  
        # 通过正则表达式匹配,判断是否结束执行  
        final_answer_match = re.search(r'Final Answer:\s*(.*)', response_text)  
        if final_answer_match:  
            final_answer = final_answer_match.group(1)  
            print("最终答案:", final_answer)  
            break  
  
        # 保存上下文  
        messages.append(response.choices[0].message)  
  
        # 通过正则表达式匹配,解析Function Calling参数  
        action_match = re.search(r'Action:\s*(\w+)', response_text)  
        action_input_match = re.search(r'Action Input:\s*({.*?}|".*?")', response_text, re.DOTALL)  
  
        # 匹配需要调用的工具  
        if action_match and action_input_match:  
            tool_name = action_match.group(1)  
            params = json.loads(action_input_match.group(1))  
            print(f"需要执行Function Calling, 工具名称: {tool_name}, 调用参数: {params}")  
  
            # 调用工具,获取执行结果  
            if tool_name == "query_fruit_unit_price":  
                observation = query_fruit_unit_price(params['fruit_name'])  
                print(f"工具的执行结果: \n{observation}", )  
  
                # 保存上下文  
                messages.append({"role": "user", "content": f"Observation: {observation}"})  

picture.image

picture.image

从执行过程可以看出,大模型主要进行了以下的推理过程:

  1. Planning:判断出需要调用工具。

  2. Action:生成实际的 Function Calling 参数。

  3. Observation: 工具调用完成后,观察执行结果,最终生成回答。

由此可见: 基于 DeepSeek 模型强大的推理能力,再结合 ReAct 框架的驱动,我们非常轻松就可以实现一个简单的 Agent。

最后,对于以上的实现做出一些提示:

  • 这里我们使用的是 DeepSeek-V3 这个 Chat Model,而并没有选择 DeepSeek-R1 这个 Reasoning Model,因为在我们的场景里,模型的推理能力是由 ReAct Prompt 驱动的,而 DeepSeek-R1 自身内置了思维链,可能与我们的 Prompt 产生冲突。

  • 最终的执行结果在不同的模型上可能有差异,特别是一些小参数模型,可能无法识别出工具调用,进而产生幻觉。

  • 在我们的实现中,工具参数调用的解析是基于正则表达式匹配来完成的,这种方式可能存在一定的误差,可以采用大模型的 Function Calling 功能来优化,但是需要对 Prompt 进行一些改造,这个工作就留给大家来完成了。

0
0
0
0
评论
未登录
暂无评论