ACP 多智能体协作实战:用 CrewAI 与 Smolagents 构建“生成—校验”研究摘要工作流(ai-engineering-hub)
本文基于 acp-code/README.md 的完整实操流程展开,演示如何通过 Agent Communication Protocol(ACP)让两个由不同框架(CrewAI 与 Smolagents)构建的 Agent 跨框架协作:第一个 Agent 为指定主题生成研究摘要初稿,第二个 Agent 借助网页搜索工具对初稿做事实核查与信息增强。读完本文,你将掌握 ACP 服务端/客户端的完整搭建方法、两个 Agent 服务的端口与模型配置细节,以及源码层面 Server/Client/@server.agent() 装饰器的调用链原理。
项目定位:跨框架的 Agent 互操作
ACP(Agent Communication Protocol)的核心价值在于“协议层解耦”:不同框架、不同语言实现的 Agent 只要遵循同一套消息与调用约定,就可以像调用远程服务一样互相协作。acp-code 模块用最小化的三文件结构展示了这一点:
| 文件 | 角色 | 运行端口 |
|---|---|---|
| crew_acp_server.py | CrewAI 构建的“研究起草”Agent 服务 | 8000 |
| smolagents_acp_server.py | Smolagents 构建的“事实核查/增强”Agent 服务 | 8001 |
| acp_client.py | ACP 客户端,串联两个服务触发工作流 | — |
工作流为典型的“生成—校验”(draft & verify)两阶段模式:research_drafter 先生成草稿,research_verifier 基于草稿在线检索最新信息并输出修订版。
环境准备:Ollama 本地模型
按照 acp-code/README.md 的说明,项目默认使用 Ollama 在本地运行 Qwen2.5 14B 模型,避免依赖外部 API。
1. 安装 Ollama 并拉取模型
# Setting up Ollama on linux
curl -fsSL https://ollama.com/install.sh | sh
# Pull the Qwen2.5 model
ollama pull qwen2.5:14b
Ollama 默认监听 http://localhost:11434,这也是后面两个 Agent 服务的 LLM 端点地址(见 crew_acp_server.py 中的 base_url 与 smolagents_acp_server.py 中的 api_base)。
2. 用 uv 初始化项目并安装依赖
要求 Python 3.10 及以上。先安装 uv:
# MacOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
然后创建项目、虚拟环境并安装依赖:
# Create a new directory for our project
uv init acp-project
cd acp-project
# Create virtual environment and activate it
uv venv
source .venv/bin/activate # MacOS/Linux
.venv\Scripts\activate # Windows
# Install dependencies
uv add acp-sdk crewai smolagents duckduckgo-search ollama
依赖的职责对应关系如下:
acp-sdk:ACP 协议的 Python SDK,提供Server、Client、Message、MessagePart等核心类型;crewai:第一个 Agent 的底层框架(角色/任务/Crew 抽象);smolagents:第二个 Agent 的底层框架(CodeAgent+ 工具调用);duckduckgo-search:为核查 Agent 提供DuckDuckGoSearchTool联网搜索能力;ollama:本地模型推理服务端。
可选:切换其他 LLM 提供商
README 同时说明可以改用 OpenAI 或 Anthropic。创建 .env 文件并写入 API Key:
OPENAI_API_KEY=your_openai_key
ANTHROPIC_API_KEY=your_anthropic_key
注意:示例代码中 LLM 是硬编码指向 Ollama 的(
model="ollama_chat/qwen2.5:14b"),若要切换提供商,需相应修改 crew_acp_server.py 中的LLM(...)与 smolagents_acp_server.py 中的LiteLLMModel(...)参数。
服务端实现一:CrewAI 的 research_drafter
crew_acp_server.py 共 35 行,完整展示了“把框架 Agent 包装为 ACP 服务”的模式。
1. LLM 配置(指向本地 Ollama)
llm = LLM(
model="ollama_chat/qwen2.5:14b", # CrewAI 中通过 LiteLLM 路由到 Ollama
base_url="http://localhost:11434", # Ollama 默认端口
max_tokens=8192 # 单次生成上限 8K tokens
)
model 字段使用 ollama_chat/ 前缀,说明 CrewAI 底层经由 LiteLLM 统一接口调用 Ollama;base_url 必须与本机 Ollama 的监听地址一致。
2. 用装饰器注册 Agent
server = Server()
@server.agent()
async def research_drafter(input: list[Message]) -> AsyncGenerator[RunYield, RunYieldResume]:
"""Agent that creates a general research summary on a given topic."""
agent = Agent(
role="Research summarizer",
goal="Draft an informative and structured research summary based on the topic",
backstory="You are a researcher who summarizes complex topics for general readers.",
llm=llm
)
task = Task(
description=f"Write a brief, clear summary on: {input[0].parts[0].content}",
expected_output="A concise paragraph summarizing the topic",
agent=agent
)
crew = Crew(agents=[agent], tasks=[task])
task_output = await crew.kickoff_async()
yield Message(parts=[MessagePart(content=str(task_output))])
几个关键设计点:
- 入口取法:ACP 客户端传入的
input是list[Message],主题文本位于input[0].parts[0].content——这是 ACP 消息体的统一访问方式; - 异步生成器签名:函数签名为
AsyncGenerator[RunYield, RunYieldResume],意味着 ACP Agent 可以流式yield中间结果;本例只yield一次最终Message,属于最简单的“一次性返回”用法; - CrewAI 侧:单 Agent 单 Task 的轻量 Crew,通过
crew.kickoff_async()异步执行(而非阻塞的kickoff()),以匹配 ACP 的 async 事件循环; - 输出封装:结果统一包装为
Message(parts=[MessagePart(content=...)]),屏蔽了 CrewAITaskOutput与 ACP 消息模型的差异。
3. 启动服务
if __name__ == "__main__":
server.run(port=8000)
服务固定监听 8000 端口,客户端即通过该地址找到 research_drafter。
服务端实现二:Smolagents 的 research_verifier
smolagents_acp_server.py 与上一个服务结构同构,但底层框架与能力完全不同——它带联网工具,负责“事实核查 + 信息增强”。
1. 模型配置(LiteLLMModel)
model = LiteLLMModel(
model_id="ollama_chat/qwen2.5:14b",
api_base="http://localhost:11434",
# api_key="your-api-key",
num_ctx=8192
)
同样经由 LiteLLM 路由到本地 Ollama。num_ctx=8192 设定了上下文窗口大小,与 CrewAI 服务中的 max_tokens=8192 在数量级上保持一致。注意 Smolagents 的对应参数名是 model_id/api_base,而 CrewAI 是 model/base_url——这正是“两个框架、同一模型后端”的具体体现。
2. 带工具的 CodeAgent
@server.agent()
async def research_verifier(input: list[Message]) -> AsyncGenerator[RunYield, RunYieldResume]:
"""Agent that fact-checks and enhances a research summary using web search."""
agent = CodeAgent(tools=[DuckDuckGoSearchTool()], model=model)
prompt = input[0].parts[0].content
response = agent.run(prompt)
yield Message(parts=[MessagePart(content=str(response))])
CodeAgent是 Smolagents 的代码执行型 Agent,tools=[DuckDuckGoSearchTool()]赋予其 DuckDuckGo 网页搜索能力——这是它能“用最新信息更新摘要”的前提;- 与 CrewAI 服务不同,这里
agent.run(prompt)是同步调用(在 async 函数内直接阻塞等待),对短任务可接受,但可推断若任务耗时较长,更稳妥的做法是在事件循环外执行或使用异步工具调用; - 服务监听 8001 端口(
server.run(port=8001)),与起草服务错开,避免端口冲突。
客户端编排:acp_client.py 串联两个服务
acp_client.py 展示了 ACP 客户端如何以“远程服务”的姿态编排跨框架工作流,全部 23 行:
import asyncio
from acp_sdk.client import Client
async def run_workflow() -> None:
async with Client(base_url="http://localhost:8000") as drafter, \
Client(base_url="http://localhost:8001") as verifier:
topic = "Impact of climate change on agriculture in 2025."
response1 = await drafter.run_sync(
agent="research_drafter",
input=topic
)
draft = response1.output[0].parts[0].content
print(f"\nDraft Summary:\n{draft}")
response2 = await verifier.run_sync(
agent="research_verifier",
input=f"Enhance the following research summary using the latest information available online providing a more accurate and updated version:\n{draft}"
)
final_summary = response2.output[0].parts[0].content
print(f"\nVerified & Enriched Summary:\n{final_summary}")
if __name__ == "__main__":
asyncio.run(run_workflow())
从源码结构看,客户端编排有四个要点:
- 两个
Client上下文管理器:分别绑定 8000/8001 端口的服务。客户端完全不感知对端是 CrewAI 还是 Smolagents——它只看到“某个 base_url 上叫research_drafter/research_verifier的 Agent”,这正是 ACP 互操作的价值所在; run_sync(agent=..., input=...):按 Agent 名称路由请求。字符串topic会被 SDK 封装为Message,与服务端的input[0].parts[0].content取法对应;- 响应取法对称:
response.output[0].parts[0].content与服务端yield Message(parts=[MessagePart(content=...)])的封装一一镜像; - 串联式数据流:第一阶段输出
draft被拼接进第二阶段的 prompt(要求“用在线最新信息增强该摘要”),形成“草稿 → 事实核查增强”的接力。客户端最终打印两段输出:先是Draft Summary,再是Verified & Enriched Summary,与 README 描述的 Output 部分(“first agent 的通用摘要 + second agent 的核查修订版”)完全一致。
运行步骤与预期输出
按 acp-code/README.md 的 Usage 说明,在两个独立终端分别启动两个 ACP 服务:
# Terminal 1
uv run crew_acp_server.py
# Terminal 2
uv run smolagents_acp_server.py
随后运行 ACP 客户端触发工作流:
uv run acp_client.py
预期输出分两段(见 acp_client.py 的 print 语句):
- Draft Summary:
research_drafter(CrewAI)对“2025 年气候变化对农业的影响”这一主题生成的通用研究摘要; - Verified & Enriched Summary:
research_verifier(Smolagents)基于 DuckDuckGo 搜索最新信息后输出的更准确、更及时的修订版摘要。
小结:这个示例说明了什么
acp-code 用一个 87 行源码(三个文件)的规模,验证了 ACP 的几个工程要点:
- 框架无关的服务封装:无论底层是 CrewAI 的
Crew/Task/Agent抽象,还是 Smolagents 的CodeAgent + tools,对外都收敛为“Server+@server.agent()异步生成器 +Message消息体”这一统一契约; - 端口即服务边界:8000/8001 两个本地端口把两个 Agent 变成可独立启动、独立部署的远程服务,客户端只做
base_url + agent 名寻址; - 消息模型双向对称:
Message(parts=[MessagePart(content=...)])的封装与output[0].parts[0].content的读取是同一数据结构的写读两端,理解这一点对扩展多 Agent 工作流(链式、并行、带条件分支)至关重要; - 本地推理 + 联网工具的组合:两个服务共用同一个 Ollama Qwen2.5 14B 后端,能力差异来自工具层(
DuckDuckGoSearchTool)而非模型层,展示了“同一模型、不同 Agent 角色”的低成本协作方式。
若要在此基础上扩展,自然的演进方向是:为核查 Agent 增加更多工具(如网页抓取)、将客户端从线性串联扩展为并行起草多主题、或在 .env 基础上把 LLM(...)/LiteLLMModel(...) 改为从环境变量读取,以便在 Ollama、OpenAI、Anthropic 之间切换。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00