ReAct认知框架及其在langchain3中实现相关解析器与示例

一、背景介绍

ReAct全称是"Reasoning and Acting",可以翻译成“推理与行动”。它来自一篇于 2023 年 ICLR 会议上发表的会议论文https://arxiv.org/pdf/2210.03629 ,ReAct相关的简介可在此查看https://react-lm.github.io/

在ReAct提出之前,大家都只是将推理与行动分开来进行研究,比如推理方面就是提升LLM的推理能力,行动方面有给LLM增加FunctionCalling的能力等,这篇论文首创性地提出将LLM的这二者结合起来协同使用。在解决问题的过程中,“reasoning traces”与“Actions”很多情况下是交错执行的,前者帮助LLM进行推断、跟踪和更新行动方案,并处理异常情况;后者则使LLM能够与外部资源(如知识库或环境)进行交互,从而获取更多信息。

有推理能力的LLM,首先需要知道有哪些外部工具可以使用(需要知道知道这些工具的作用、输入形式、输出结果情况)。ReAct这个认知框架的大致过程是:当LLM理解用户问题后,会进行一步步推导和感知,在某一步与LLM的交互过程中得知需要调用外部工具并找到可以调用的工具、准备好工具调用时需要的输入信息,然后在下一步调用工具、传递工具需要的输入参数、获得工具调用结果,(所有与LLM的交互的消息都会被加入历史消息中,供LLM在后续查看与使用。所以如果没有干预的话,在某问题下,这个历史消息会不断增加甚至突破LLM的上下文窗口限制,这是ReAct的一个缺点),LLM查看、感知这次的工具调用的结果及前面的历史消息再次进行思考与推理,如果有需要就不断重复“思考与推理”、“行动与行动输入”、“感知”这些步骤,直到某次调用完工具并获得结果后,查看、感知所有历史消息后终于发现已经得到了原始问题的最终结果,然后它不再要求调用工具了,而是对所有历史消息特别是最后一步得到的结果进行综合总结,然后向用户返回最终的回复内容。

只推理、只行动的认知框架或处理模式如下图左边所示,ReAct认知框架如下图右边所示。

image-20250824213317360

二、react_agent默认结果解析器

langchain作为一个开源且流行的AI应用开发框架,已经实现了ReAct相关的agent,在使用react agent的过程中,需要特别注意的是处理好提示词与自定义函数即工具。函数需要编写明确的功能描述、入参与返回结果描述,而提示词推荐使用英文,且根据不同厂商的模型可能还需要进行一定的调试。以下对langchain3中“从LLM的某次回复结果中提取行动与运行输入即Action与Action Input”的相关源码进行简单梳理。

当我们使用langchain3中的langchain.agents.create_react_agent来创建一个react agent工具时,可以传递一个output_parser 参数,即可以自定义LLM结果解析器,它是可选的,如果不传递,默认使用的是 langchain.agents.output_parsers.ReActSingleInputOutputParser,它的源码实现如下,最重要的是其中的parse方法,以下对其parse方法的官方实现做了一些注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
class ReActSingleInputOutputParser(AgentOutputParser):
"""Parses ReAct-style LLM calls that have a single tool input.

Expects output to be in one of two formats.

If the output signals that an action should be taken,
should be in the below format. This will result in an AgentAction
being returned.

```
Thought: agent thought here
Action: search
Action Input: what is the temperature in SF?
```

If the output signals that a final answer should be given,
should be in the below format. This will result in an AgentFinish
being returned.

```
Thought: agent thought here
Final Answer: The temperature is 100 degrees
```

"""

def get_format_instructions(self) -> str:
return """Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question"""

def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
# FINAL_ANSWER_ACTION的值是"Final Answer:",判断text变量中是否包含"Final Answer:"
includes_answer = FINAL_ANSWER_ACTION in text
"""
正则表达式匹配模式:
* 表示匹配前面的子表达式0到多次。
\s* 表示匹配任何空白字符,包括空格、制表符、换页符、换行等等,匹配0到多次。
\d* 表示匹配任意一个阿拉伯数字(0 到 9),匹配0到多次。
(.*?) 表示非贪婪匹配(也称为惰性匹配或最小匹配)是指正则表达式引擎在匹配时会尽可能少地匹配字符,只要满足匹配条件就立即停止。这与默认的贪婪模式(尽可能多地匹配字符)形成鲜明对比
(.*) 表示贪婪匹配。在此处就是贪婪匹配工具输入参数,凡是“:[\s]*”后面的内容教会被匹配到
"""
regex = (
r"Action\s*\d*\s*:[\s]*(.*?)[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
)
#使.能匹配换行符,实现跨行匹配
action_match = re.search(regex, text, re.DOTALL)
#如果正则模式确实在text变量中匹配到了
if action_match:
#如果text中包含"Action:"与"Final Answer:",那么text中不可能包含"Final Answer:",否则抛出异常
if includes_answer:
raise OutputParserException(
f"{FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE}: {text}"
)
action = action_match.group(1).strip() #提取匹配到的工具名称,具体是“(.*?)”匹配到的内容
action_input = action_match.group(2) #提取匹配到的工具输入,具体是“(.*)”匹配到的内容
tool_input = action_input.strip(" ") #去掉工具输入前后的空格和单个双引号
tool_input = tool_input.strip('"')
#返回一个AgentAction对象,LLM得到这个结果后会执行Action Input,真正地调用相关工具并传参
return AgentAction(action, tool_input, text)

# action_match为None,且text中包含"Final Answer:",说明已经获取了最终的答案,返回一个AgentFinish对象
elif includes_answer:
return AgentFinish(
{"output": text.split(FINAL_ANSWER_ACTION)[-1].strip()}, text
)

# 没有从text中同时匹配到"Action:"与"Final Answer:",也没有获取到最终答案
if not re.search(r"Action\s*\d*\s*:[\s]*(.*?)", text, re.DOTALL):
# 没有从text中匹配到"Action:",抛出异常"Invalid Format: Missing 'Action:' after 'Thought:'"
raise OutputParserException(
f"Could not parse LLM output: `{text}`",
observation=MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE,
llm_output=text,
send_to_llm=True,
)
elif not re.search(
r"[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)", text, re.DOTALL
):
# 没有从text中匹配到"Action Input:",抛出异常"Invalid Format: Missing 'Action Input:' after 'Action:'"
raise OutputParserException(
f"Could not parse LLM output: `{text}`",
observation=MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE,
llm_output=text,
send_to_llm=True,
)
else:
# 其他情况(text中既没有"Action:"也没有"Final Answer:"),抛出异常"Could not parse LLM output: `{text}`"
raise OutputParserException(f"Could not parse LLM output: `{text}`")

@property
def _type(self) -> str:
return "react-single-input"

三、react_agent完整调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from dotenv import load_dotenv
from langchain_community.tools import TavilySearchResults
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.agents import create_react_agent
from langchain.agents import AgentExecutor, tool
import numexpr
import math
import os
load_dotenv() #自己准备TavilySearch时需要用到的api key与DASHSCOPE_API_KEY

# 初始化大模型
llm = ChatOpenAI(api_key=os.getenv("DASHSCOPE_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", model="qwen-plus", temperature=0.3)

@tool
def calculator(expression: str) -> str:
"""使用Python的 numexpr 库计算表达式。表达式应该是解决问题的单行数学表达式。.
例子:
"37593 * 67"
"37593**(1/5)"
"37593^(1/5)"
"""
local_dict = {"pi": math.pi, "e": math.e}
return str(
numexpr.evaluate(
expression.strip(),
global_dict={},
local_dict=local_dict, # 添加常用数学函数
)
)
# 设置工具
search = TavilySearchResults(max_results=3)
tools = [calculator, search]

# 设置提示模板
"""提示词中三个变量是必须的:
tools:包含每个工具的描述和参数。
tool_names:包含所有工具名称。
agent_scratchpad:以字符串形式包含以前的智能体操作和工具输出。
"""
template = '''
Answer the following questions as best you can. You can use the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should think about what to do
Action: the name of the tool to use, should be one of [{tool_names}]
Action Input: the input to the tool
Observation: the result of the tool
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: Now I know the final answer
Final Answer: the final answer to the original question

Begin!

Question: {input}
Thought: {agent_scratchpad}'''

"""以下是上面提示词的中文意思,注意!这里只能用英文提示词!
因为 LangChain 在处理时是根据英文单词来进行的,用中文的话会出错,
如果想使用中文提示词,必须自定义?"""
# template = ('''
# '尽你所能回答以下问题。如果能力不够你可以使用以下工具:'
# '{tools}
# 使用以下格式:'
# '问题:你必须回答的输入问题'
# '思考:你应该始终思考该做什么'
# '行动:要采取的行动,应该是 [{tool_names}]之一'
# '行动输入:行动的输入'
# '观察: 行动的结果'
# '... (思考/行动/行动输入/观察可以重复N次)'
# '思考: 我现在知道最终答案了'
# '最终答案: 原始输入问题的最终答案'
# '开始!'
# '问题: {input}'
# '思考:{agent_scratchpad}'
# '''
# )
prompt = PromptTemplate.from_template(template)

# 初始化Agent
agent = create_react_agent(llm=llm,
tools=tools,
prompt=prompt)

# 构建AgentExecutor
agent_executor = AgentExecutor(agent=agent,
tools=tools,
handle_parsing_errors=True,
verbose=True)

# 执行AgentExecutor
agent_executor.invoke({"input": """在中国,目前市场上玫瑰花的一般进货价格是多少?大概是多少钱一支?\n如果我在此基础上加价5%,应该如何定价?"""})

执行结果大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> Entering new AgentExecutor chain...
我需要搜索中国市场上玫瑰花的一般进货价格。
Action: tavily_search_results_json
Action Input: 中国 市场 玫瑰花 进货价格
Observ[{'title': '情人节玫瑰花涨势汹涌:进口货现“天价”一束国产货输在品质但占主流', 'url': 'https://www.neabridge.com/chwsjj/201702/2328823.html', 'content': '不过,记者也了解到,在目前国内市场上,因为进口玫瑰价格高昂的原因,玫瑰花的销售仍然以国产为主。\n\n记者走访位于广州芳村的岭南花卉市场时发现,九成以上的花卉批发商买的玫瑰都是国产货,货源来自广州、昆明等地。一位主要销售国产玫瑰的店主张老板表示,今年预订进口玫瑰的客户跟去年相比减少了六成以上,以至于他们不敢大批量进货。\n\n深圳市一位花店店主也在接受记者采访时表示,除去部分高端店,多数花店更乐意销售国产玫瑰花,因为销售国产花更赚钱,“进口花进价要接近100块一支,售价很高也很难卖出去,除了一些土豪,很少有人买,如果卖不出去就成了我们店里的损耗,这个损耗是非常严重的。”\n\n国产玫瑰差在哪里\n\n与进口玫瑰高价相对应的是,国产玫瑰批发价最高只能到4~5元/支。2017年2月12日昆明斗南花卉批发市场的鲜花报价显示,国产玫瑰中,同一品种高等级的批发价在3~4元每支左右,而部分品种低等级玫瑰鲜花产品批发价低至1.5元/支。昆明斗南花卉批发市场是国内最大的花卉批发市场。 [...] 《每日经济新闻》记者2月12日走访广州芳村的岭南花卉市场了解到,广州批发市场并未出现如同成都市三圣乡花卉批发市场60元/支的“天价”进口玫瑰。在记者找到的3家进口花卉批发店中,进口玫瑰的价格均在80~200元/束(一束为10支)之间,每支批发价不到20元。\n\n“今年情人节的进口玫瑰卖得挺好的。”广州珠江新城一家主营厄瓜多尔进口玫瑰的花店老板谢先生也向记者表示,厄瓜多尔“甜心玫瑰”和“自由女神”两款批发价均是20元一支。\n\n记者走访过程中了解到,虽然广州花卉市场未出现每支逼近百元的“天价”,但深圳也有花店店主告诉记者,进口花的进价确实要到100元/支。Roseonly官网2月12日产品售价显示,roseonly的最低价一束也要999元,内含12支玫瑰鲜花,相当于每支约83元;价格最高为9999元,为一束99支的鲜花玫瑰产品,每支价格为101元。根据Roseonly品牌所属公司诺誓(北京)商业股份有限公司(以下简称诺誓商业)的公开股份转让说明书,Roseonly玫瑰鲜花花源正是厄瓜多尔进口玫瑰。 [...] 情义花卉公司相关负责人告诉记者,“正常的情况下,国产稀奇品种每支批发价在3~4元/支,个别品种这两天涨到了5~6元/支,但是很少。”\n\n诺誓商业去年提交的新三板公开转让说明书显示,公司的主营业务为玫瑰鲜花、玫瑰永生花、珠宝首饰等高端礼品的设计、加工和销售,公司2015年毛利率为62.28%。公司的玫瑰鲜花花材主要选用来自厄瓜多尔的玫瑰。\n\n记者了解到,目前国内高端玫瑰主要是进口玫瑰。为何进口玫瑰价格比国产高出这么多?\n\n前述广州珠江新城进口花店老板谢先生认为,进口玫瑰的价格高昂一方面是因为运输关税成本高;另一方面是因为它们的质量优秀且有保障。进口玫瑰的花形饱满,花瓣数量多,颜色也更加鲜艳亮丽。同时,进口玫瑰的质量稳定,花期平均为14天,是国产玫瑰的两倍。\n\n相比之下,国产玫瑰在情人节期间经常缺货,供货商为了盈利提前收割未成熟的玫瑰,导致质量参差不齐。“来这边的消费者都有比较高的消费能力,他们对花的要求也高一点,更多会选进口玫瑰。”谢先生说。', 'score': 0.60274047}, {'title': '这几天,玫瑰花价格创新高', 'url': 'http://www.jj.gov.cn/art/2022/2/15/art_1311043_59066633.html', 'content': '记者发现,平时不过8元一枝的普通红玫瑰,已经迅速换上“节日价”。“今年的玫瑰花挺贵的,红玫瑰15元起步,而往年情人节也就10元左右。”黄老板说,按照这两天的进价,按扎出售的玫瑰花价格也相应上涨。目前寓意一生一世11枝的玫瑰花束售价360元、寓意爱的最高点19枝的花束售价430元、寓意三生三世33枝的玫瑰花束售价620元、寓意我爱你52枝的玫瑰花束售价780元。“一朵花估计涨5元至8元,因为进价比同期翻了三至五倍。”\n\n在江城南路的一家鲜花绿植店,店主说,今年情人节玫瑰花最贵了,往年情人节的价格比平时涨价30%左右,但今年的玫瑰花仅进价就涨了三到五倍,零售价只好再往上走,卖出了十多年来的最高价。\n\n那么,是什么原因导致玫瑰花的进价如此之高呢?黄女士告诉记者,鲜花价格受多种因素影响,作为商品,鲜花价格受供求关系影响;作为特殊农产品,鲜花价格又受到天气影响;作为表情达意的载体,鲜花也受到婚庆、节庆等节日影响。她店内的鲜花多来自昆明,受前段时间霜冻影响,玫瑰产量明显下降,加之新年期间婚庆用花较多,出现短期供不应求,玫瑰价格随之而涨。', 'score': 0.5735118}, {'title': '鲜花批发、昆明云花苑基地鲜花直供网、云南鲜花批发、昆明鲜花 ...', 'url': 'http://yunhuayuan.net/', 'content': '\\ 首页\n\\ 今日花价\n\\ 订购指南\n\\ 运输需知\n\\ 花店知识\n\\ 花卉常识\n\\ 汇款账号\n\\ 留言板\n\\ 关于我们\n### 玫瑰鲜花批发 2025-07-24(今日)更多玫瑰花批发价格\n花卉名称 颜色 单位 等级 价格 图文\n高原红红色 20支/扎 A-B-C级 18-13-8元Image 1\n卡罗拉红色 20枝/扎 A-B-C级 17-12-7元Image 2\n黑魔术黑红色 20枝/扎 A-B-C级 16-12-8元Image 3\n艳粉(影星)粉色 20枝/扎 A-B-C级 17-13-9元Image 4\n戴安娜粉色 20枝/扎 A-B-C级 16-12-8元Image 5\n坦尼克白色 20枝/扎 A-B-C级 15-12-9元Image 6\n雪山白色 20枝/扎 A-B-C级 18-14-10元Image 7\n卡布奇诺咖啡色 20枝/扎 A-B-C级 18-13-8元Image 8\n金枝玉叶黄色 20枝/扎 A-B-C级 20-16-12元Image 9\n海洋之歌淡紫色 20枝/扎 A-B-C级 16-12-8元Image 10', 'score': 0.5037359}]根据搜索结果,中国市场上玫瑰花的进货价格如下:

- 国产玫瑰花的批发价格一般在1.5元至5元/支之间,特殊品种或高等级的玫瑰价格可能达到5元至6元/支。
- 在特殊节日(如情人节)期间,由于需求增加和供应受限,国产玫瑰花的进价可能会显著上涨,例如达到8元至15元/支。

如果在目前的玫瑰花进货价格基础上加价5%,定价方式如下:

1. 假设目前玫瑰花的平均进货价格为4元/支。
2. 加价5%:$ 4 \times (1 + 0.05) = 4.2 $元/支。

因此,定价可以为4.2元/支。

Final Answer:
1. 玫瑰花的进货价格一般在1.5元至5元/支,特殊节日可能上涨至8元至15元/支。
2. 如果加价5%,以4元/支为基准,定价可以为4.2元/支。

> Finished chain.

ReAct认知框架及其在langchain3中实现相关解析器与示例
https://jiangsanyin.github.io/2025/08/24/ReAct认知框架及其在langchain3中实现相关解析器与示例/
作者
sanyinjiang
发布于
2025年8月24日
许可协议