多 Agent 协作的代码组织:为什么我把工具和 Agent 分成两个文件
起因
跑通多 Agent 协作后,我的项目里有两个核心文件:
text
/opt/rag-nvidia/ ├── real_tools.py ← 工具定义 ├── golf_agent.py ← Agent 编排 └── langgraph.json ← 指向 golf_agent.py:app
一开始我把所有代码写在一个文件里,后来拆成了两个。这篇解释为什么这么拆。
两个文件的分工
| 文件 | 角色 | 内容 |
|---|---|---|
real_tools.py | 工具定义层 | 三个 @tool 函数:搜索、生图、发布 |
golf_agent.py | Agent 编排层 | 创建三个 Agent,把工具分配给它们,用 Supervisor 调度 |
一句话:real_tools.py 是「工具箱」,golf_agent.py 是「工人」。
结合方式:一行 import
golf_agent.py 的第一行就是关键:
python
from real_tools import search_golf_news, generate_golf_image, publish_to_wordpress
这一行把 real_tools.py 里定义的三个工具导入到 golf_agent.py,然后分配给对应的 Agent。
工具层:real_tools.py
这个文件只管「怎么调用外部服务」。每个工具是一个 @tool 装饰的函数:
工具一:搜索
python
@tool
def search_golf_news(query: str) -> str:
"""从 golf.com RSS 抓取高尔夫新闻"""
resp = requests.get("https://golf.com/feed/", timeout=30)
root = ET.fromstring(resp.content)
results = []
for item in root.findall(".//item")[:5]:
title = item.find("title").text
link = item.find("link").text
results.append(f"标题:{title}\n原文链接:{link}")
return "\n\n".join(results)
这个函数不关心谁调用它,只关心「怎么从 RSS 抓到新闻」。
工具二:生图
python
@tool
def generate_golf_image(title: str) -> str:
"""用 Pollinations 免费接口生成高尔夫配图"""
prompt = f"Golf course, professional photography, {title}"
encoded = urllib.parse.quote(prompt)
url = f"https://image.pollinations.ai/prompt/{encoded}?width=1200&height=630"
img_data = requests.get(url, timeout=120).content
path = f"/tmp/golf_{abs(hash(title))}.jpg"
with open(path, "wb") as f:
f.write(img_data)
return path
同样,它不关心谁调用,只关心「怎么生成图片并保存到本地」。
工具三:发布
python
@tool
def publish_to_wordpress(title: str, content: str, image_path: str) -> str:
"""把文章和配图发布到 golforfun.net 草稿箱"""
# 上传图片 → 获取 media_id
# 创建草稿 → 返回链接
...
这个函数封装了 WordPress REST API 的所有细节——认证、媒体上传、文章创建。
这三个工具的共同点:它们都是纯粹的功能函数,不知道 Agent 的存在,只做一件事。
编排层:golf_agent.py
这个文件只管「谁用什么工具、按什么顺序执行」。
第一步:导入工具
python
from real_tools import search_golf_news, generate_golf_image, publish_to_wordpress
第二步:创建 Agent,分配工具
python
# Researcher 拿到搜索工具
researcher = create_agent(
model=model,
tools=[search_golf_news], # ← 来自 real_tools.py
name="researcher",
system_prompt="你是高尔夫新闻调研员...",
)
# Writer 拿到生图 + 发布工具
writer = create_agent(
model=model,
tools=[generate_golf_image, publish_to_wordpress], # ← 来自 real_tools.py
name="writer",
system_prompt="你是高尔夫内容撰写与发布专家...",
)
# Reviewer 不需要工具,只用 LLM 审核
reviewer = create_agent(
model=model,
tools=[], # ← 空列表
name="reviewer",
system_prompt="你是内容审核专家...",
)
关键点:Agent 本身不实现任何功能,它只是「决定什么时候调用哪个工具」。真正的功能在 real_tools.py 里。
第三步:Supervisor 调度
python
workflow = create_supervisor(
agents=[researcher, writer, reviewer],
model=model,
prompt="你是高尔夫内容团队主管。流程:先调研,再写稿,最后审核。",
)
app = workflow.compile()
数据流向
当你运行 python golf_agent.py 时,发生这些事:
text
1. golf_agent.py 启动
↓
2. 从 real_tools.py 导入三个工具函数
↓
3. 把工具分配给对应的 Agent
↓
4. Supervisor 调度:
↓
researcher 调用 search_golf_news()
↓ (返回新闻标题、链接、摘要)
writer 调用 generate_golf_image()
↓ (返回本地图片路径)
writer 调用 publish_to_wordpress()
↓ (返回草稿链接)
reviewer 审核结果
↓
5. 输出最终结果
为什么要分成两个文件
理由一:改工具不用动 Agent
假设你想把 RSS 源从 golf.com 换成 Golfweek:
只改 real_tools.py:
python
resp = requests.get("https://golfweek.usatoday.com/feed/", timeout=30)
golf_agent.py 完全不用动。
理由二:改 Agent 不用动工具
假设你想加一个新的 SEO Agent:
只改 golf_agent.py:
python
seo_agent = create_agent(
model=model,
tools=[], # SEO Agent 不需要外部工具
name="seo_agent",
system_prompt="你是 SEO 专家,优化标题和关键词。",
)
workflow = create_supervisor(
agents=[researcher, writer, seo_agent, reviewer], # 加进来
...
)
real_tools.py 完全不用动。
理由三:换服务不用重写逻辑
假设你想把生图服务从 Pollinations 换成 DALL-E:
只改 real_tools.py:
python
@tool
def generate_golf_image(title: str) -> str:
# 换成 DALL-E 的调用逻辑
...
golf_agent.py 里的 tools=[generate_golf_image] 一行都不用改。
理由四:方便测试
你可以单独测试每个工具,不需要启动整个 Agent 系统:
python
# test_real_tools.py
from real_tools import search_golf_news, generate_golf_image, publish_to_wordpress
result = search_golf_news.invoke({"query": "PGA Tour"})
print(result)
这种「单独测试工具」的能力,在调试时非常有用。
一个常见的坑:文件必须在同一目录
from real_tools import ... 要求 real_tools.py 在当前工作目录或 Python 路径里。
所以你必须这样运行:
bash
cd /opt/rag-nvidia source /opt/langgraph-studio-venv/bin/activate python golf_agent.py
如果报 ModuleNotFoundError: No module named 'real_tools',说明你不在 /opt/rag-nvidia/ 目录下。
更进一步的拆分
如果你想让项目更清晰,还可以继续拆:
text
/opt/rag-nvidia/ ├── tools/ │ ├── __init__.py │ ├── search.py ← 搜索工具 │ ├── image.py ← 生图工具 │ └── wordpress.py ← 发布工具 ├── agents/ │ ├── __init__.py │ ├── researcher.py │ ├── writer.py │ └── reviewer.py ├── golf_agent.py ← 主入口 └── langgraph.json
但这是「过度设计」——对于三个工具、三个 Agent 的项目,两个文件就够了。
当前项目结构
text
/opt/rag-nvidia/ ├── real_tools.py ← 三个工具函数(搜索、生图、发布) ├── golf_agent.py ← 三个 Agent + Supervisor ├── langgraph.json ← LangGraph Studio 配置 ├── .env ← 环境变量(LangSmith Key) └── test_real_tools.py ← 工具单独测试脚本
一句话总结
工具层只负责「怎么做」,Agent 层只负责「谁来做、什么时候做」。 两层通过 import 结合,各自独立演化,互不干扰。这是多 Agent 项目最基础也最重要的代码组织原则。
