12-day3-ReAct Agent-实验3-交互式-6个tools
✅ 升级版 react_agent_2.py
✅ 新增功能:
- 本地冷笑话库:程序启动时自动创建
jokes.txt(若不存在),内含 10 个程序员冷笑话 tell_a_joke工具:从本地文件随机读取一条笑话get_my_ip_info工具:查询公网 IP 和地理位置(使用ipinfo.io)run_safe_shell_command工具:安全执行白名单内的 shell 命令
✅ 安全 & 鲁棒性:
- 外挂幽默笑话
- Shell 命令严格白名单 + 超时 + 输出截断
- 所有工具带异常处理
tee jokes.txt <<'EOF'
我问奶奶:“您年轻时怎么追爷爷的?”
她眯着眼说:“哪用追?他欠我五块钱,三十年没还,只好娶我了。”
朋友失恋后去庙里求签,大师说:“放下。”
他点点头,把前女友送的金镯子摘下来放功德箱里,转身就走。
大师急了:“我不是让你放下这个!”
我妈坚信WiFi辐射会让人变傻。
昨天她拔了路由器,结果我爸立刻清醒过来,第一件事就是问:“老婆,你藏私房钱那罐茶叶在哪?”
相亲对象问我:“你有什么缺点?”
我说:“太诚实。”
她说:“这不算缺点。”
我说:“你丑。”
小区电梯里,一位大妈盯着我看半天,突然说:
“小伙子,你长得好像我儿子!”
我感动地说:“真的吗?”
她叹口气:“可惜他三年前就去世了……”
我赶紧安慰她。
她接着说:“不过没关系,他留下的房贷,现在由你继承。”
老板说:“公司就是你的家!”
第二天我就把办公椅搬回家了。
他说:“你怎么能这样?”
我说:“你说这是我家啊。”
女朋友生日,我送她一本《如何读懂男人的心》。
她翻了两页,抬头问我:“你确定这是新书?怎么第38页折了角?”
我说:“……那是我昨晚试读时折的。”
我爸退休后沉迷短视频,最近总对我说:
“儿子,你要努力!你看人家马化腾,40岁就财务自由了!”
我说:“爸,马化腾今年53。”
他愣了一下:“那更要抓紧了!只剩13年了!”
医生对病人说:“你得戒烟戒酒,早睡早起,保持心情愉快。”
病人问:“然后呢?”
医生说:“然后你就能活得久一点——多受几年罪。”
小时候偷吃糖被妈妈发现,她问:“是不是你?”
我摇头。
她说:“那糖怎么少了?”
我说:“可能是糖自己想出去看看世界。”
现在我在做产品经理,专门给APP加“自动续费”功能。
EOF
tee react_agent_3.py <<'EOF'
# react_agent_3.py - 增强交互式 ReAct Agent
import os
import platform
import random
import subprocess
from datetime import datetime
from dotenv import load_dotenv
from langchain_core.tools import tool
from langchain.agents import AgentExecutor, create_react_agent
from langchain import hub
from langchain_openai import ChatOpenAI
import requests
# 加载 API Key
load_dotenv()
# ────────────────────────────────────────────────
# 🔧【工具 1:查询包在清华源是否可用】
# ────────────────────────────────────────────────
@tool
def check_package_on_tsinghua_mirror(package_name: str) -> str:
"""
检查指定的 Python 包是否在清华大学 PyPI 镜像中可用。
示例输出: "numpy: ✅ 可用,最新版本 1.26.4"
"""
try:
url = f"https://pypi.tuna.tsinghua.edu.cn/{package_name}/json"
headers = {"User-Agent": "langchain-agent"}
response = requests.get(url, headers=headers, timeout=8)
if response.status_code == 200:
data = response.json()
version = data["info"]["version"]
return f"{package_name}: ✅ 可用,最新版本 {version}"
elif response.status_code == 404:
return f"{package_name}: ❌ 未在 PyPI 或清华源中找到"
else:
return f"{package_name}: ⚠️ 镜像服务返回错误 {response.status_code}"
except Exception as e:
return f"查询失败: {str(e)}"
# ────────────────────────────────────────────────
# 🕒【工具 2:获取当前时间】
# ────────────────────────────────────────────────
@tool
def get_current_time(dummy: str = "") -> str:
"""获取当前日期和时间(中国标准时间)"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return f"{now} (CST)"
# 💻【工具 3:获取详细操作系统信息】
@tool
def get_os_info(dummy: str = "") -> str:
"""返回当前操作系统的详细类型和版本"""
system = platform.system()
if system == "Windows":
return "Windows"
elif system == "Darwin":
return "macOS"
elif system == "Linux":
release_files = ["/etc/os-release", "/etc/lsb-release", "/etc/redhat-release", "/etc/debian_version"]
for filepath in release_files:
try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
content = f.read().strip()
if not content:
continue
if filepath == "/etc/os-release":
props = {}
for line in content.splitlines():
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
props[k] = v.strip('"').strip("'")
name = props.get("PRETTY_NAME") or props.get("NAME") or "Linux"
version = props.get("VERSION", "")
full = f"{name} {version}".strip()
return full if full else "Linux"
else:
first_line = content.split("\n")[0].strip()
if first_line:
return first_line
except (FileNotFoundError, PermissionError, OSError):
continue
return "Linux"
else:
return f"其他系统: {system}"
# 🌍【工具 4:获取公网 IP 和地理位置】
@tool
def get_my_ip_info(dummy: str = "") -> str:
"""获取当前公网 IP 及大致地理位置(基于 ipinfo.io)"""
try:
response = requests.get("https://ipinfo.io/json", timeout=5)
if response.status_code == 200:
data = response.json()
ip = data.get("ip", "未知")
city = data.get("city", "")
region = data.get("region", "")
country = data.get("country", "")
loc = ", ".join(filter(None, [city, region, country])) or "未知位置"
return f"公网 IP: {ip},位置: {loc}"
else:
return "IP 查询服务暂不可用"
except Exception as e:
return f"IP 查询失败: {str(e)}"
# 💀【工具:讲一个本地幽默故事】
@tool
def tell_a_joke(dummy: str = "") -> str:
"""
从本地 jokes.txt 随机讲一个幽默故事。
文件格式:每个故事占一个或多个连续非空行,故事之间用空行分隔。
"""
try:
with open("jokes.txt", "r", encoding="utf-8") as f:
content = f.read()
# 按空行(\n\n 或更多)分割段落,并清理每段
paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
if not paragraphs:
return "故事库为空,请检查 jokes.txt 是否有内容。"
story = random.choice(paragraphs)
return "🎭 幽默时间:\n" + story
except FileNotFoundError:
return "未找到 jokes.txt,请先创建它!"
except Exception as e:
return f"加载故事失败: {str(e)}"
# 🖥️【工具 6:安全执行 Shell 命令(白名单)】
@tool
def run_safe_shell_command(cmd: str) -> str:
"""
执行安全的 shell 命令(仅限白名单)
支持: 'ls', 'pwd', 'whoami', 'date', 'df', 'free', 'uname'
"""
allowed = {
"ls": ["ls", "-l"],
"pwd": ["pwd"],
"whoami": ["whoami"],
"date": ["date"],
"df": ["df", "-h"],
"free": ["free", "-h"],
"uname": ["uname", "-a"]
}
cmd_key = cmd.strip().split()[0] if cmd.strip() else ""
if cmd_key not in allowed:
return f"不支持的命令: '{cmd}'。可用命令: {', '.join(allowed.keys())}"
try:
result = subprocess.run(
allowed[cmd_key],
capture_output=True,
text=True,
timeout=5,
cwd=os.getcwd()
)
output = (result.stdout or result.stderr).strip()
# 截断长输出
if len(output) > 800:
output = output[:800] + "...\n(输出已截断)"
return output if output else "命令执行成功,无输出。"
except subprocess.TimeoutExpired:
return "命令执行超时"
except Exception as e:
return f"命令执行失败: {str(e)}"
# ────────────────────────────────────────────────
# 🧠【LLM 配置:使用 Qwen via DashScope】
# ────────────────────────────────────────────────
llm = ChatOpenAI(
model="qwen-max",
temperature=0,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
# 注册所有工具(按逻辑分组)
tools = [
check_package_on_tsinghua_mirror,
get_current_time,
get_os_info,
get_my_ip_info,
tell_a_joke,
run_safe_shell_command
]
# ────────────────────────────────────────────────
# 📜【加载 ReAct 提示模板】
# ────────────────────────────────────────────────
prompt = hub.pull("hwchase17/react")
# ────────────────────────────────────────────────
# 🤖【创建 ReAct Agent】
# ────────────────────────────────────────────────
agent = create_react_agent(llm, tools, prompt)
# ────────────────────────────────────────────────
# ⚙️【创建执行器】
# ────────────────────────────────────────────────
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
handle_parsing_errors=True,
max_iterations=8 # 支持更复杂的多工具组合
)
# ────────────────────────────────────────────────
# 💬【交互式主循环】
# ────────────────────────────────────────────────
print("🚀 ReAct Agent 已启动!支持以下能力:")
print(" • 查询 PyPI 包(清华源)")
print(" • 获取当前时间")
print(" • 识别操作系统详情")
print(" • 查询公网 IP 与地理位置")
print(" • 讲一个程序员冷笑话")
print(" • 安全执行 shell 命令(ls/pwd/whoami/df/free/uname)")
print("\n💡 尝试提问:")
print(" - '我在哪?'")
print(" - '讲个笑话!'")
print(" - '当前目录有什么文件?'")
print("\n输入 'quit' 或 'exit' 退出\n")
while True:
try:
user_input = input("❓ 你: ").strip()
if not user_input:
continue
if user_input.lower() in ("quit", "exit", "q"):
print("👋 再见!")
break
print("\n🧠 Agent 正在思考...\n")
result = agent_executor.invoke({"input": user_input})
print("\n" + "=" * 60)
print("🤖 Agent 回答:")
print(result["output"])
print("\n" + "—" * 60 + "\n")
except KeyboardInterrupt:
print("\n\n👋 用户中断,再见!")
break
except Exception as e:
print(f"\n❌ 运行出错: {e}\n")
EOF
✅ 新增能力一览
| 工具 | 调用方式 | 示例问题 |
|---|---|---|
tell_a_joke |
本地文件 | “讲个笑话!”、“我需要快乐” |
get_my_ip_info |
网络 API | “我在哪?”、“我的公网 IP 是多少?” |
run_safe_shell_command |
白名单命令 | “当前目录有什么?”、“磁盘空间还剩多少?” |
🔒 安全说明
- Shell 命令:仅允许 7 个无害命令,禁止任意执行
- 文件读取:仅读取当前目录下的
jokes.txt,无写入或遍历 - 网络请求:所有外部调用均有超时和异常捕获
浙公网安备 33010602011771号