多 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.pyAgent 编排层创建三个 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 项目最基础也最重要的代码组织原则。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注