DeepSeek + LangGraph: 探讨并评测推理模型在Agent应用上的能力与表现

既然DeepSeek-R1擅长深度“思考”,那么在更需要自主规划能力的Agent应用中,它是否有更好的表现呢?本文将以最常见的ReAct范式的Agent来探讨这个问题,不过,结果可能会颠覆你的预期。

  • 回顾:什么是ReAct Agent?
  • ReAct Agent需要哪些推理能力?
  • 手搓一个支持DeepSeek-R1的ReAct Agent(基于LangGraph)
  • 不同模型的ReAct Agent评测
  • 思考与总结
01

什么是ReAct Agent?

ReAct(Reasoning + Acting)是一种结合推理(Reasoning)行动(Acting)的 AI 智能体范式。其核心理念是让 AI 先进行思考推理,再据此采取行动,如调用外部工具;经过多次这样循环,直到任务完成。

图片

简单的说就是:

给Agent安上手脚(Tools)和大脑(LLM),让它自己思考,并借助手脚完成任务。

所以,ReAct Agent的特点是:

  • 自主规划能力:自行推理多子任务步骤,并在过程中动态调整

  • 工具使用:支持与API、数据库、搜索引擎等交互,以完成任务

  • 更高的灵活性:相对于有固定流程的Workflows,任务更灵活

  • 存在不可控性:LLM输出的不确定性可能会带来一定不可控性

图片
Anthropic在《Build effective agents》中将Agentic系统分成了有着预定流程的Workflows与自主规划的Agents,很显然ReAct Agents属于后者。

02

ReAct Agent需要哪些推理能力

ReAct Agent是一种高度依赖于LLM的智能体,其需要的推理能力包括:

* 多任务步骤的规划与推理

  • 根据输入任务与工具等信息推理出子任务,并合理安排顺序
  • 理解推理历史与观察工具结果,动态规划,调整决策,避免冗余等

比如这样的任务:

“了解公司新品的网络评价,如果偏负面,则发送邮件到产品经理。”

这个任务需要LLM最终能够规划出类似下面的任务步骤,注意这个规划是要在过程中动态完成的,因为你无法事先得知网络评价的结果:

图片

* 工具使用的推理

能够推理子任务需要使用的工具,及其结构化的输入参数。这通常借助LLM的Function Calling特性或者提示工程(Prompt)来实现。

* 协助子任务的执行

除了任务步骤的推理,当然LLM也可以用来协助完成某个具体任务。比如在一个研究智能体中,辅助生成论文提纲等。

我们重点关注前两种推理能力,这是体现Agent自主能力的核心。

03

手搓支持DeepSeek-R1的ReAct Agent

一个典型的ReAct Agent的Graph图表示如下:

图片

在LangGraph中构建ReAct Agent最快捷的方法是使用官方提供的prebuilt函数:

图片

但这个方法对我们并不适用。因为这个方法有一个强制要求:

大模型必须支持Function Calling(函数调用),因为其内部实现需要借助LLM的Function Calling功能来推理下一个步骤与工具!而目前的推理模型都不支持函数调用。

所以,我们必须要使用LangGraph来自行实现这个Agent,这需要使用提示工程来要求LLM进行任务步骤推理。这里仅展示核心过程:

1. 提示词

设计ReAct范式的Prompt如下:

...

# ReAct 提示格式
REACT_PROMPT = '''
你被设计用于帮助完成各种输入任务,包括回答问题、内容创作、自动化处理等。

## 工具
你可以使用各种工具,并且需要自行决定使用工具的顺序,以完成当前任务。
这可能需要将任务拆分为多个子任务,并使用不同的工具来完成各个子任务。
你可以使用以下工具:

{tools_desc}

## 输出格式
如果本次需要使用工具完成某个子任务,请按照以下格式输出:
```
Thought: 我需要使用一个工具来帮助回答这个问题。
Action: 工具名称 (从 {tool_names} 中选择一个工具),如果需要使用工具的话。
Action Input: 传递给工具的输入,使用 JSON 格式表示参数(例如:{{"query": "你好"}})。
```
注意:
* 始终以“Thought”开头。
* 绝对不要在你的响应外部使用 Markdown 代码标记,但如果在你的回答中需要,你可以在适当的位置使用代码标记。
* 请使用有效的JSON 格式作为Action Input。不要使用类似 {{'input': '你好世界', 'num_beams': 5}} 这种错误格式。
* 一次响应最多只能使用一个工具以完成一个子任务。不要在一次响应中出现多个Action。

如果你已经获得足够的信息而来输出最终回答,则必须使用以下两种格式之一来输出最后答案:
```
Thought: 我可以在不使用更多工具的情况下回答问题。
Answer: [你的回答 (与用户问题的语言相同)]
```
```
Thought: 我无法使用提供的工具回答该问题。
Answer: [你的回答 (与用户问题的语言相同)]
```
注意:
* 确保所有子任务都完成后才输出最终答案
* 最终回答尽量涵盖用户任务的所有方面


## 当前对话
以下是当前的对话历史, 由人类、AI的消息交替组成。

'''

2. 准备Tools

准备几个简单的模拟Tool,交给Agent使用:

  • 网络搜索(network_search)
  • 计算器(caculator)
  • 文本摘要与总结(document_summarizer)
  • 电子邮件发送(Email)

代码大致如下(省略了内部逻辑):

...

# 定义4个模拟工具
@tool
def network_search(query: str) -> str:
    """
    用于执行网络搜索并返回搜索结果
    Args:
        query (str): 要进行网络搜索的关键词
    """
    print_colored(f'调用tool: network_search, 输入: {query}', 'green')
    try:
        # 调用 TavilySearchResults 来执行搜索,模拟返回结果
        results = TavilySearchResults(max_results=2).invoke({"query": query})
        return f"搜索结果: {results}"
    except Exception as e:
        return f"搜索错误: {str(e)}"
    
@tool
def calculator(query: str) -> str:
    """
    用来执行加减乘除计算
    Args:
        query (str): 要计算的表达式,如1+2*3
    """
  ...

@tool
def document_summarizer(text: str) -> str:
    """
    用来提炼与总结文本的核心内容,形成摘要
    Args:
        text (str): 要总结的文本内容
    """
  ...
@tool
def email(recipient, subject, body) -> str:
    """
    用于发送电子邮件
    Args:
        recipient (str): 收件人的电子邮件地址
        subject (str): 电子邮件的主题
        body (str): 电子邮件的正文
    """
...

3. 实现核心推理逻辑(借助LLM)

这个是整个流程的核心部分,主要职责是调用LLM进行任务推理:

  1. 通过提示词与对话历史让LLM推理下一步行动
  2. 如果需要调用工具,则给出结构化的调用信息
  3. 如果无需再调用工具,则输出LLM给出的答案
  4. 如果达到最大迭代次数仍未完成任务,则报错

实现如下(代码中的llm将在后面测试中使用不同模型如Deepseek-R1来替换):

...
llm = ChatOpenAI(model='gpt-4o-mini')
tools = [network_search,calculator,document_summarizer,email]

async def call_model(
    state: State, config: RunnableConfig
) -> Dict[str, List[AIMessage]]:
    
    # 生成工具描述
    tools_desc = "\n".join([
        f"- {tool.name}: {tool.description}\n" 
        for tool in tools
    ])

    tool_names = [tool.name for tool in tools]

    # 生成 ReAct 提示
    system_prompt = REACT_PROMPT.format(
        tools_desc=tools_desc,
        tool_names=tool_names
    )

    # 这一步需要根据不同模型增加或注释
    state.messages = [HumanMessage(content=msg.content) if "ToolMessage" in msg.__class__.__name__ else msg for msg in state.messages]

    # 调用 LLM,输入提示词(system_prompt)和对话历史(messages)
    response = await llm.ainvoke([SystemMessage(content=system_prompt)] + state.messages)
    content = response.content
    
    print_colored("\n===========================Reasoning================================", 'magenta')
    print_colored(f'{content}', 'magenta')
    print_colored("=========================Reasoning End================================\n", 'magenta')

    if "Action:" in content and "Action Input:" in content:

        # 提取工具调用信息
        action_lines = [line for line in content.split('\n') if line.startswith('Action:') or line.startswith('Action Input:')]
        tool_name = action_lines[0].replace('Action:', '').strip()
        tool_input = action_lines[1].replace('Action Input:', '').replace("'",'"').strip()
        tool_input = tool_input.replace('\\', '\\\\')
        
        response.tool_calls = [{
            "id": "call_1",
            "type": "function",
            "name": tool_name,
            "args": json.loads(tool_input)
            }
        ]

    if state.is_last_step and response.tool_calls:
        return {
            "messages": [
                AIMessage(
                    content="对不起,我在指定的步骤数内无法找到问题的答案。"
                )
            ]
        }

    return {"messages": [response]}

4. 创建Graph

完成了核心的推理节点,就可以创建这个Graph(Tools调用借助官方ToolNode组件即可):

@dataclass
class InputState:
    messages: Annotated[Sequence[AnyMessage], add_messages] = field(
        default_factory=list
    )

@dataclass
class State(InputState):

    is_last_step: IsLastStep = field(default=False)

builder = StateGraph(State, input=InputState)
builder.add_node(call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge("__start__", "call_model")

# 定义一个函数,根据模型输出的内容来决定下一个节点;如果有工具调用,则转向tools
def route_model_output(state: State) -> Literal["__end__", "tools"]:
    last_message = state.messages[-1]
    if not isinstance(last_message, AIMessage):
        raise ValueError(
            f"Expected AIMessage in output edges, but got {type(last_message).__name__}"
        )
    
    if not last_message.tool_calls:
        return "__end__"
    
    return "tools"

# 添加条件边
builder.add_conditional_edges(
    "call_model",
    route_model_output,
)
builder.add_edge("tools", "call_model")

# 编译
graph = builder.compile()
graph.name = "ReAct Agent"

5. 验证可用性

现在你可以使用graph.invoke方法来输入任务,以测试这个ReAct Agent。按照预期,它应该能够理解你的自然语言输入任务,并使用已有的四个工具(搜索、计算器、摘要、邮件发送)来完成。为了更好的观察,我们加入一些跟踪信息,以展示LLM的推理过程。

这里先用一个简单任务看下效果(模型gpt-4o-mini):

图片

可以看到,第一次推理,LLM认为需要使用搜索工具;第二次推理,因为观察到工具返回(这里未打印),LLM已经可以正确回答问题了!

04

不同模型的ReAct Agent评测

现在我们使用不同难度的任务来考察类似DeepSeek- R1这样的推理模型,在ReAct Agent中的多任务步骤推理时的表现,并同通用模型做对比。

测试任务(从简单到复杂)

  • "搜索《哪吒2》的最新票房数据",
  • "搜索《哪吒2》的详细剧情介绍,并发送到test@gmail.com",
  • "搜索《哪吒2》的最新票房,如果超过50亿,再帮我搜索《哪吒3》的上映时间; 如果没有超过50亿,则创作一段加油的文字。最后把结果发邮件到test@gmail.com"

对比模型:

  • gpt-4o-mini
  • doubao-1.5-32k
  • deepseek-v3(官方)
  • deepseek-r1(官方)
  • deepseek-r1:7b(开源)
  • qwen-2.5:7b(开源)

测试方法:

  • 在不改变其他代码与提示的情况下,切换不同的模型
  • 依次输入3个测试任务,每个任务测试多次,观察结果
  • 确保每次任务是独立的,不记忆之前的任务历史 

测试结果:

任务一 任务二 任务三
gpt-4o-mini
Yes
Yes Yes
doubao-1.5-32k yes yes yes
deepseek-v3
yes yes yes
deepseek-r1
yes yes *  no
deepseek-r1:7b yes no *
no
qwen-2.5:7b yes yes
no
  • yes: 全部成功,且推理过程输出符合预期
  • no:全部失败,未获得期望结果甚至异常
  • yes*: 代表大部分成功,偶尔失败;或者结果成功,但推理过程不符预期
  • no*: 代表大部分失败,偶尔成功

DeepSeek-R1失败的主要现象:

1. 不遵循逐步推理的指令。即一次性返回包含多个Thought-Action行动步骤的完整任务过程(尽管这个过程可能是正确的)。如这次:

图片

2. 基于假设(幻觉)推理。在第一步的子任务推理后,有时候会自作主张的“假设”一个工具调用结果,并据此做后续步骤推理(r1:7b模型)。

3. 遗漏某个任务步骤。比如某一次任务3的测试中,未推理与执行“发送邮件”这一步骤,但最终却有如下“貌似完美”的输出:

图片

4. 输出格式不能稳定遵循指令要求。比如下图中,自行添加了'json'标记,当然格式的问题有时候可以通过代码来容错,但也可能会直接异常:

图片

我们来看一下最复杂的任务3的成功输出,推理过程应该类似下图:

图片

05

思考与总结

尽管从直觉上你可能会认为“自带Cot(思维链)”的推理模型Deepseek- R1应该更擅长自主的多步骤任务规划与推理,但是事实恰恰相反:

目前的推理模型(DeepSeek-R1、OpenAI的o1等)在Agent需要的自主、多步骤、动态的任务规划能力上还缺乏足够的可用性

下面是我们的一些具体思考与总结:

1. 目前o1,r1这样的推理模型的确擅长step by step的思考问题,但输入任务应该更直接。即复杂性体现在问题本身,而不能是提示词与上下文。比如:

  • 根据需求描述生成复杂的代码
  • 解决一道复杂的数学问题
  • 进行一项科学计算与研究
  • 根据主题生成有深度的论文

而以下类型的任务它目前并不擅长:

  • 基于复杂上下文与对话历史的推理任务
  • 有严格的指令遵循与结构化输出要求的任务
  • 依赖于Function Calling功能的智能体任务

这里的结构化输出可以借助辅助LLM来完善;Function Calling也可以借助提示词来获得,但是第一个问题则需要等待模型进化。

2. 具体到AI Agent应用,特别是ReAct Agent,由于其更依赖于LLM的多任务步骤推理能力。我们建议:

  • 在任务步骤的动态规划与推理上,通用模型目前更适合,比如GPT-4o,DeepSeek-v3;且参数越大的模型越稳定
  • 推理模型,包括满血的DeepSeek-R1,以及开源的1.5b等蒸馏版本,都不建议用作Agent的任务步骤推理;但是可以作为一个Tool来完成某个子任务,比如根据搜索结果编写一份有深度的报告

3. 推理模型在Agent中应用的另一个问题是响应性能,由于其深度思考的特点,其响应时间普遍较长,这是制约其在企业级应用的另一个问题。

以上的一些结论刚好也验证了DeepSeek-R1官方技术报告揭示的自身“能力缺陷”,包括不支持function calling、不擅长多轮对话、结构化输出较弱、提示词与复杂上下文敏感等,我们也期待下一个版本的R1在这些能力上取得突破!

来源:AI大模型应用实践

THE END