
100行代码手写一个reAct Agent
序
什么是agent,在LLM Powered Autonomous Agents这篇文章中的总结就很好,Agent=LLM+Memory+Plan+Tool Use
那么如何去完成一个实实在在能解决问题的agent呢?在之前,我想可能是去学习一个agent框架,例如langchain,去阅读他的文档,了解框架用法,再去拆解问题,写代码
但是在读了Building Effective AI Agents这篇文章后,按照A社的指南,如无必要,勿增实体
最佳的实践是在实现一个Agent的时候,尽量保持它的简洁,最成功的实现是使用简单,可组合的模式,而不是复杂的框架
这些复杂的框架的封装会导致你失去对底层的控制,掩盖底层的响应和提示,使调试变得困难
另外,它也会导致简单的问题变的复杂
那么,手写一个reAct Agent就变的很重要了,既然针对问题我们第一步不应该想到框架,我们应该把握好对于Agent底层的写法,而不是对于任何问题都使用庞大的框架,于是我写了一个小玩具。
下面是本文的代码仓库地址:
https://github.com/Maizsh/reAct-agent-demo
目标
我们的目标是实现一个一百行代码左右的reAct Agent,他拥有两个工具,一个数学工具可以计算,另外一个工具是web搜索可以让他获取外部信息,以便于这个Agent可以回答诸如2024年广州的gdp是多少人民币?换算成美元是多少美元?这种问题。
实现过程
先理解 ReAct 的本质:
while True:
response = llm(messages) # 问 LLM
if response 想调工具:
result = 执行工具(response) # Act
messages.append(result) # Observe
continue # 回到 Think
else:
print(response) # 最终回答
break
大道至简,所有Agent的底层本质都是这个while循环
那么思路就很清晰了
LLM初始化
按照Agent的定义,首先我们实现LLM这个环节
#注意OpenAI,AI都是大写
from openai import OpenAI
client = OpenAI(
base_url = "",
api_key = ""
)
工具初始化
之后按照顺序,我们先完成两个工具的书写
def calculator(expression:str)->str:
try:
result = eval(expression)
return str(result)
except Exception as e:
return f"计算错误:{e}"
def web_search(query: str) -> str:
import re
try:
resp = httpx.get(
"https://html.duckduckgo.com/html/",
params={"q": query},
headers={"User-Agent": "Mozilla/5.0"},
timeout=10.0,
)
# 提取标题和摘要
titles = re.findall(r'class="result__a".*?>(.*?)</a>', resp.text)
snippets = re.findall(r'class="result__snippet".*?>(.*?)</a>', resp.text, re.DOTALL)
results = []
for i in range(min(3, len(titles))):
title = re.sub(r'<.*?>', '', titles[i]).strip()
snippet = re.sub(r'<.*?>', '', snippets[i]).strip() if i < len(snippets) else ""
results.append(f"[{i+1}] {title}\n {snippet}")
return "\n\n".join(results) if results else "未找到相关结果"
except Exception as e:
return f"搜索错误: {e}"
在计算器方法中,直接使用python的eval去执行代码,这是非常危险的,容易被攻击,所以只适用于本地演示,反正我们只是做个小玩具嘛
在web_search这个方法中,我们使用duckduckgo搜索引擎,因为它无需配置api key,非常适合我们这个小玩具项目,这样我们就可以把所有精力放到实现agent中了
在完成工具的逻辑代码后,我们需要定义工具的schema,这一步是为了告诉LLM有哪些工具可用
tools = [
{
"type": "function",
"function": {
"name": "calculator",
"description": "计算数学表达式,如 '2+3*4' 或 'sqrt(144)'",
"parameters": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "数学表达式"}
},
"required": ["expression"]
}
}
},
{
"type": "function",
"function": {
"name": "web_search",
"description": "使用 Bing 搜索需要的内容",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "检索内容"}
},
"required": ["query"]
}
}
}
]
实现Agent loop
消息初始化
这是Agent实现最关键的一步,首先定义方法,添加两条初始化的消息
def agent(user_input: str):
messages = [
{"role": "system", "content": "你是一个有工具可用的助手。需要搜索或计算时调用工具。"},
{"role": "user", "content": user_input}
]
按照OpenAi的规范,role的合法值共有四种
[table]
system message必须放到第一条,虽然没有强制规定,但是如果不放在第一条效果会变差且只能有一条
message列表里的assistant与user必须交替出现,不然会行为异常
user的content可以是数组或者字符串,数组是用来多模态
函数分发表
tool_fuctions = {
"calculator":calculator,
"web_search":web_search,
}
把工具名映射到相应的py函数,方便之后调用
func = tool_fuctions[name] # name 是 LLM 返回的字符串,如 "calculator"
result = func(**args) # 通过字典拿到函数,然后调用
如果没有这个分发表,就只能使用if elif,丑得很,而且如果后期加工具,也只需在分发表里加一行,逻辑不需要动
if name == "calculator":
result = calculator(**args)
elif name == "web_search":
result = web_search(**args)
while循环
做了如此多的工作,我们终于要来到核心部分,loop循环,先放代码,再一一解释
for i in range(10):
#1.调用llm
response = client.chat.completions.create(
model = "gpt-5.5",
messages = messages,
tools = tools
)
choice = response.choices[0]
if(choice.finish_reason=="tool_calls"):
print(choice.message.tool_calls)
messages.append(choice.message)
for tool_call in choice.message.tool_calls:
name = tool_call.function.name
id = tool_call.id
args = json.loads(tool_call.function.arguments)
func = tool_fuctions[name]
result = func(**args)
messages.append(
{
"role":"tool",
"tool_call_id":id,
"content":str(result)
}
)
print(f"问题:{choice.message},工具: {name}, 结果: {result}")
continue
elif(choice.finish_reason=="stop"):
print(f"结束:{choice}")
break
else:
print(f"异常:{choice.message}")
pass
10次循环是因为我们目前只是个玩具项目,防止它陷入死循环
每一轮的流程是调用llm,判断是否使用工具,是的话使用工具,把结果放入message返回,这也是最简单memory机制,如果不用的话就结束整个while循环了
client
对于这个实例,有以下用法
client.chat.completions.create() # 对话
client.embeddings.create() # 向量嵌入
client.images.generate() # 图片生成
client.audio.transcriptions.create() # 语音转文字
client.models.list() # 列出可用模型
response
在response这个返回对象中,有以下字段
response.id # 本次请求的唯一ID,如 "chatcmpl-xxx"
response.model # 实际使用的模型名
response.created # 时间戳
response.object # 固定值 "chat.completion"
response.choices # 回复列表(重点)
response.usage # token 用量统计
response.usage — token 消耗
response.usage.prompt_tokens # 输入消耗的 token 数
response.usage.completion_tokens # 输出消耗的 token 数
response.usage.total_tokens # 合计
response.choices — 核心内容
通常只有一个元素,代码里取的是 choices[0]:
choice = response.choices[0]
choice.index # 序号,一般是 0
choice.finish_reason # 结束原因(见下表)
choice.message # 模型返回的消息对象
finish_reason 的取值
[table]
choice.message — 消息对象
choice.message.role # 固定是 "assistant"
choice.message.content # 文字回复内容,tool_calls 时一般是 None
choice.message.tool_calls # 工具调用列表,stop 时是 None
tool_calls 里每个元素的结构
tool_call.id # 工具调用ID,回传给 tool role 时要用
tool_call.type # 固定 "function"
tool_call.function.name # 函数名,如 "calculator"
tool_call.function.arguments # JSON 字符串,需要 json.loads() 解析
完结撒花🎉
如此一来,我们就完成了一个最基础的Agent,感谢你的阅读~