AI 代理:掌握3个关键模式(ReAct)。第2部分,共3部分
Source: Dev.to
很抱歉,我无法直接访问外部链接来获取文章内容。请您将需要翻译的文本粘贴在这里,我会帮您翻译成简体中文,并保持原有的格式、Markdown 语法以及技术术语不变。
文章 [第 1 部分]
这些模式的代码已在 GitHub 上公开。 [Repo]
“工具使用”模式(文章 1)
我们给 AI 手来与外部世界交互。
使用 ReAct,我们在为它装配一个 功能性大脑。
回顾
-
在文章 1 中,流程是完全线性的:
Stimulus → Response你请求一条数据,代理触发了一个工具,就这样结束。
没有疑问,没有计划。 -
现实世界很混乱。
当一次点击不足以完成任务会怎样?
如果你需要 调查、比较数据,然后 进行计算?
这时线性模型会崩溃,ReAct(由普林斯顿和谷歌的研究者在 2022 年提出)便派上用场。它基本上是今天的行业标准。
名称来源于 Reasoning + Acting(推理 + 行动)。
突破性的想法不仅是使用工具(我们已经有了);我们 强制 LLM 进行内部独白——一个 思考轨迹。
代理不再盲目冲刺回答,而是进入认知循环:
代理实际上在日志中 自言自语。
它根据刚刚发现的内容规划下一步,使其能够 在执行过程中即时纠正方向,如果事情未按预期进行。
工作原理 – 示例
用户查询: “法国现任总统的年龄的平方根是多少?”
| 迭代 | 操作 | 观察结果 |
|---|---|---|
| 1 | search_tool("Current President of France") | 返回 “Emmanuel Macron”。 |
| 2 | search_tool("Emmanuel Macron age") | 返回 “47”。 |
| 3 | calculator_tool("sqrt(47)") | 返回 “6.85”。 |
| 最终答案 | — | “法国总统 Emmanuel Macron(年龄 47)的平方根约为 6.85。” |
看到魔法了吗? 第 3 步如果没有先发现第 2 步是做不到的。代理一直在 连接各个点。
使用 Agno 实现 ReAct 模式
import os
import sys
import logging
import traceback
from typing import List, Optional
from dotenv import load_dotenv, find_dotenv
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.tavily import TavilyTools
# ----------------------------------------------------------------------
# 1. Global Logging and Error Handling Configuration
# ----------------------------------------------------------------------
LOG_DIR = os.path.join(os.path.dirname(__file__), "log")
LOG_FILE = os.path.join(LOG_DIR, "logs.txt")
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(LOG_FILE, encoding="utf-8"),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
def global_exception_handler(exctype, value, tb):
"""Capture unhandled exceptions and record them in the log."""
error_msg = "".join(traceback.format_exception(exctype, value, tb))
logger.error(f"Unhandled exception:\n{error_msg}")
sys.__excepthook__(exctype, value, tb)
sys.excepthook = global_exception_handler
# ----------------------------------------------------------------------
# 2. Environment Variables Loading
# ----------------------------------------------------------------------
env_path = find_dotenv()
if env_path:
load_dotenv(env_path)
logger.info(f".env file loaded from: {env_path}")
else:
logger.warning(".env file not found")
# ----------------------------------------------------------------------
# 3. Tool Definitions
# ----------------------------------------------------------------------
def calculate(expression: str) -> str:
"""
Solve a simple mathematical expression (addition, subtraction,
multiplication, division). Useful for calculating year or date differences.
Args:
expression (str): e.g. "2024 - 1789"
"""
try:
# Allow only safe characters for basic eval
allowed_chars = "0123456789+-*/(). "
if all(c in allowed_chars for c in expression):
result = eval(expression)
return f"Result: {result}"
else:
return "Error: Disallowed characters in expression."
except Exception as e:
return f"Error while calculating: {str(e)}"
# ----------------------------------------------------------------------
# 4. Agno Agent Configuration (ReAct Pattern)
# ----------------------------------------------------------------------
model_id = os.getenv("BASE_MODEL", "gpt-4o")
agent = Agent(
model=OpenAIChat(id=model_id),
tools=[TavilyTools(), calculate],
instructions=[
"You are a researcher using the ReAct (Reason + Act) method.",
"1. **Think** step‑by‑step about what information you need to answer the user's question.",
"2. Use the search tool (Tavily) to find specific dates, facts, or data.",
"3. Use the calculator ('calculate') for any mathematical operation or time calculation.",
"4. Do not guess historical information. If you don't have a piece of data, look it up.",
"5. Show your reasoning clearly: 'Thought:', 'Action:', 'Observation:'.",
"6. Continue investigating until you have a complete and verified answer."
],
)
# ----------------------------------------------------------------------
# 5. User Interface
# ----------------------------------------------------------------------
def main():
logger.info("Starting Historical Detective Agent (ReAct)...")
print("--- Historical Detective - ReAct Pattern ---")
print("Type 'exit' to quit.\n")
while True:
try:
user_input = input("Researcher, what is your question?: ")
if user_input.lower() == "exit":
Source: …
logger.info("The user has ended the session.")
break
if not user_input.strip():
continue
logger.info(f"User query: {user_input}")
print("\nInvestigating...\n")
agent.print_response(user_input, stream=True, show_tool_calls=True)
print("\n")
except KeyboardInterrupt:
logger.info("Keyboard interrupt detected.")
break
except Exception as e:
logger.error(f"Error in main loop: {str(e)}")
print(f"\nAn error occurred: {e}")
if __name__ == "__main__":
main()
ReAct 在代码中的位置?
| 组件 | 如何实现 ReAct |
|---|---|
| THINK | 在代理的 instructions 中定义——第一条子弹说明模型需要逐步思考并展示其推理过程(Thought:)。 |
| ACT | tools 列表(TavilyTools() 和 calculate)提供了代理可以执行的动作。说明中明确写有“使用搜索工具 …”以及“使用计算器 …”。 |
| OBSERVE | 每次工具调用后,代理会收到一个观察(工具的输出),并可以在下一轮迭代中引用它。 |
| REPEAT | 循环是隐式的:模型会持续生成 Thought → Action → Observation 循环,直至满足最终指令(“继续调查直到得到完整且经验证的答案”)。 |
工具中定义的外部能力
tools = [TavilyTools(), calculate]
OBSERVE: 框架(Agno)执行工具并将结果返回给代理。
Agent class: 代理“读取” calculate 的返回值(例如 Result: 6.85)。
REPEAT: 循环会一直进行,直到代理拥有足够的信息为止。
agent.print_response(..., show_tool_calls=True)
show_tool_calls=True非常关键:它让你在控制台实时看到代理是如何“触发”动作的。
为什么我们如此喜欢这种模式?(优点)
- 解决多跳问题 – 代理可以回答 A 导致 B,B 导致 C 的问题。
- 自我修复能力 – 如果搜索失败(例如,“马克龙的年龄”),普通脚本会崩溃。ReAct 代理会思考:“搜索失败了。我会查找他的出生日期并自行计算年龄。”
- 告别黑箱 – 作为开发者,你可以阅读 Thoughts,确切了解代理为何选择计算器(或其他工具)。
- 减少幻觉 – 通过强制每一步都基于真实的观察(
Observation),模型更难捏造数据。
缺点与注意事项
- 它很慢(延迟) – ReAct 是顺序执行的。思考三次意味着要调用 LLM 三次 加上 执行工具。预计会有 10–30 秒 的等待。
- API 费用(成本) – “内部独白”会消耗大量 token,因此你的上下文历史会很快被填满。
- 循环风险 – 代理可能会陷入执念:
I search for X → Not there → I should search for X again → I search for X… - 精细提示 – 你需要一个调校良好的系统提示,让模型知道何时停止思考并给出最终答案。
Production Tips (Agno, LangChain, etc.)
-
给它设限——最大迭代次数
始终设置迭代上限(例如 10 步)。如果问题在 10 步内没有解决,放到 100 步也不会奇迹般地解决,只会浪费金钱。 -
教会它何时停止——停止序列
大语言模型必须在触发 Action 之后立即停止输出。如果不截断,它会自行捏造 Observation(即产生工具结果的幻觉)。现代框架通常会处理此问题,但仍需留意。 -
清理垃圾——上下文管理
在长对话中,删除旧的思考痕迹,只保留最终答案。否则代理会立刻耗尽“RAM”(上下文窗口)。
示例用例
- 编码代理(例如 Devin、Copilot) –
Write code → Execute → See error → Think how to fix → Rewrite - 二级技术支持 –
Read ticket → Check server status → Check user logs → Cross‑reference data → Answer - 金融分析师 –
Search current price → Search breaking news → Compare with history → Generate recommendation
编码愉快! 🤖