首页>AI>正文

手写ReAct智能体:记忆分层与工具调用实战

Feng 28 阅读 AI

先想清楚Agent到底多了什么

普通对话模型只做一件事:你问,它答。Agent不一样,它拿到一个目标后自己决定下一步做什么,做完看结果,再决定下一步。差别集中在四点:能主动拆解目标、能调外部工具、能记住中间过程、出错后能自己纠偏。

这四点里,工具是能力上限,记忆是稳定性的下限。很多Demo跑两轮就崩,问题基本都出在这两块,而不是模型不够强。

ReAct循环:思考、动手、看结果

ReAct是目前最省心的架构,没有花哨的状态机,就是一个循环:模型先推理当前该干什么,选一个动作,执行,把结果塞回上下文,再判断目标是否达成。

while not task_completed:
 thought = llm.think(current_state, goal)
 action = llm.choose_action(thought)
 result = tool.execute(action)
 observation = parse_result(result)
 memory.add(thought, action, observation)
 task_completed = llm.is_completed(goal, observation)

循环本身简单,难点在于退出条件。只靠模型自己说“完成了”不可靠,必须叠加最大步数硬上限,通常5到10步就够,超过还没收敛,说明任务拆解或工具设计有问题,继续跑只是烧钱。

记忆分三层,别混在一起

短期记忆就是当前对话上下文,受模型窗口限制,4K到128K token不等。工作记忆放当前任务的中间结果,用内存里的字典或列表存,容量不受限但进程一停就没了。长期记忆存历史经验和领域知识,落到向量库,检索时按需召回。

工程上最容易犯的错是把所有东西往上下文里塞。正确做法是:工具返回的长结果先摘要再入上下文,历史轮次超阈值就压缩成要点,长期知识只在需要时检索注入。分层不是为了好看,是为了控制token成本。

工具注册:Schema写全,否则必炸

工具决定了Agent能干什么。搜索、代码执行、文件读写、API调用、浏览器自动化,这几类覆盖了大多数场景。注册工具时,OpenAI function calling要求提供完整的JSON Schema,参数名、类型、是否必填都得写清楚。

原文示例里 properties 是空对象,这在真实调用中会出问题:模型不知道参数结构,生成的 arguments 经常缺字段,func(args) 直接抛 TypeError。修复方式是把每个参数的 schema 补全,并在执行前用 pydantic 模型校验一遍入参,校验失败就把错误信息作为 observation 回灌给模型,让它重试。

另一个高频坑是SDK版本。OpenAI Python SDK 1.x 之后,接口从 functions/function_call 换成了 tools/tool_calls,chat.completions.create 的参数结构也变了。网上大量旧教程的代码在新版上跑不通,报的是参数不识别。迁移时注意:工具定义放 tools 数组,响应里取 message.tool_calls,回填结果用 role: "tool" 加 tool_call_id。这三处对齐了,旧代码基本能跑。

一个能跑的最小实现

下面这个类只做三件事:维护消息列表、把工具调用分发出去、控制步数。

import json
from openai import OpenAI

class SimpleAgent:
 def __init__(self, api_key, model="gpt-4"):
 self.client = OpenAI(api_key=api_key)
 self.model = model
 self.memory = []
 self.tools = {}

 def register_tool(self, name, func, description):
 self.tools[name] = {"func": func, "desc": description}

 def run(self, task, max_steps=10):
 self.memory.append({"role": "user", "content": task})
 for step in range(max_steps):
 response = self.client.chat.completions.create(
 model=self.model,
 messages=self.memory,
 tools=self._get_tool_schemas()
 )
 msg = response.choices[0].message
 self.memory.append(msg)
 if msg.tool_calls:
 for tool_call in msg.tool_calls:
 result = self._execute_tool(tool_call)
 self.memory.append({
 "role": "tool",
 "tool_call_id": tool_call.id,
 "content": str(result)
 })
 else:
 return msg.content
 return "Agent reached max steps without completing task."

 def _execute_tool(self, tool_call):
 name = tool_call.function.name
 args = json.loads(tool_call.function.arguments)
 func = self.tools[name]["func"]
 return func(args)

 def _get_tool_schemas(self):
 return [{"type": "function", "function": {
 "name": name, "description": t["desc"],
 "parameters": {"type": "object", "properties": {}}
 }} for name, t in self.tools.items()]

注意 _get_tool_schemas 里的 properties 仍需按实际工具补全,上面保留原样只是为了对照问题所在。

挂上工具,让它真能干活

注册几个典型工具,Agent就有了实际能力:

def web_search(query: str) -> str:
 import requests
 resp = requests.get(f"https://api.search.brave.com/res/v1/web/search?q={query}")
 return resp.json()["web"]["results"][0]["description"]

def calculate(expression: str) -> str:
 return str(eval(expression))

def read_file(filepath: str) -> str:
 with open(filepath, 'r') as f:
 return f.read()

def write_file(filepath: str, content: str) -> str:
 with open(filepath, 'w') as f:
 f.write(content)
 return f"Written {len(content)} chars to {filepath}"

agent = SimpleAgent(api_key="your-key")
agent.register_tool("web_search", web_search, "Search the web for information")
agent.register_tool("calculate", calculate, "Evaluate a math expression")
agent.register_tool("read_file", read_file, "Read a local file")
agent.register_tool("write_file", write_file, "Write content to a local file")

result = agent.run("帮我调研2024年最流行的5个AI Agent框架,并写一份对比报告保存到report.md")
print(result)

这里有个必须点出来的问题:calculate 用 eval 执行模型生成的表达式,等于把任意代码执行权交给了模型输出。生产环境不能这么写。要么换成 ast.literal_eval 只允许字面量,要么用受限的表达式解析库,只放行四则运算和数学函数。这个工具适合本地实验,不适合暴露给外部输入。

框架怎么选,看你要解决什么

LangChain生态最全,文档多,适合通用开发,但抽象层厚,出问题排查链路长。CrewAI主打多角色协作,任务需要分工时用它省事。AutoGen是微软的多Agent对话框架,代码生成类任务表现不错。Dify走低代码可视化路线,快速验证想法合适,深度定制会受限。OpenAI Assistants API最省心,但绑定官方平台。Phidata轻量,内置工具多,适合做原型。

选型原则很朴素:先看任务是不是多Agent协作,是就CrewAI或AutoGen;不是就看团队工程能力,能接受重抽象用LangChain,要快用Dify或Phidata。别为了用框架而用框架,一百行能解决的事没必要引依赖。

工业场景的实现形态

智能运维Agent采集PLC/SCADA数据,用异常检测模型判断状态,再由LLM分析根因,自动开工单、推通知。质量分析Agent从MES/QMS拉SPC数据,算CpK、识别趋势、生成报告。工艺优化Agent采集参数和产品数据,建代理模型找最优参数组合,再做A/B验证。知识管理Agent本质是RAG,理解问题、检索知识库、生成带引用的答案。

这几类场景的共同点是:感知源明确、动作可枚举、结果可验证。反过来,目标模糊、动作空间开放、结果无法自动判定的任务,现阶段不适合交给Agent。

踩过的坑和对应解法

死循环最常见,Agent反复调同一个工具。解法是设最大步数,外加对工具调用参数做去重检测,连续两次相同调用直接中断。

幻觉表现为编造不存在的信息。要求工具结果必须引用来源,关键结论走事实核查工具,比在prompt里写“不要编造”有效得多。

上下文溢出是长任务的天敌。工具返回先摘要,历史轮次分层压缩,别指望模型自己记住。

成本失控靠预算控制。简单判断用小模型,复杂推理才上大模型,工具结果做缓存,token预算设硬上限。

优化上还有两个实用技巧:每三步让Agent自检一次是否偏离目标;输出走流式,用户感知延迟大幅下降。

收尾的判断

Agent的公式不复杂:模型加记忆加工具加规划。ReAct够用,工具设计决定能力边界,记忆分层决定能跑多远。工业场景是当前实现最实的区域,因为边界清晰、结果可验证。但别把它当万能,它擅长的是有明确判定标准的重复性任务,超出这个范围,人工兜底仍然是必需的。

ID:6846

Feng
这位作者很神秘,还没有填写简介。