AgentScope 2.0 学习笔记:Agent 编排初体验(零基础跑通第一个智能体)
系列:AgentScope 2.0 学习笔记 · 第 001 篇
难度:入门 · 零基础友好
适合谁:会调大模型 API、但还没做出过"真 Agent"的开发者
阅读收获:理解 Agent 的「编排」思想,亲手跑通第一个会对话的智能体
运行环境:WSL Ubuntu-24.04 + Python 3.11+ + AgentScope 2.x
很多新手卡在同一个地方:ChatGPT 用得很溜,OpenAI SDK 也能调通,但一说到"做个 Agent",就不知道从哪下手。不是不会写代码,是缺一个「拼装」的思路——大模型只是零件,Agent 是把零件装成能干活的东西。
这篇就是干这个的:给你一套最小骨架,把 AgentScope 2.0 的「编排」讲透,代码全贴、复制就能跑。跑通了,你手里就有第一块能往上继续盖房子的地基。
一、先扫盲:LLM、Token、Agent 分别是什么
在动手之前,先花一分钟把三个词说清楚,这是理解后面所有内容的地基。
LLM(大语言模型):就是你常听到的 DeepSeek、混元、通义千问。你可以把它想成一个「读过海量文字、特别会接话」的大脑。你给它一句话,它根据概率预测出最合理的下一句。
Token(词元):模型读文字时,不是按「字」读的,而是切成一小块一小块的「词元」。中文里大约一个汉字是 1~2 个 Token。为什么关心它?因为大模型按 Token 计费,Token 用得越多越贵。后面很多优化,本质都是在省 Token。
Agent(智能体):光有一个大脑还不够,你还得给它定身份(你是客服还是老师)、给它工具(能不能查库存)、给它记忆(记不记得刚才说了啥)。「大脑 + 身份 + 工具 + 记忆」组装出来的完整角色,才是 Agent。 本文的「编排」,就是组装它的手艺。
二、为什么叫「编排」,而不是「调用」
很多人第一次玩大模型,写出来的是这样的代码:
resp = client.chat.completions.create(model="gpt-4", messages=[{"role": "user", "content": "你好"}])
print(resp.choices[0].message.content)
这不叫 Agent,这只是一次性的「调用模型」——问一句、答一句,答完就散伙,没有身份、没有记忆、没有工具。
真正的 Agent,要能连续多轮对话、调用工具、查知识库、甚至多个智能体互相协作。把这些零件像搭积木一样组织起来的过程,就叫「编排」(Orchestration)。「编排」和「调用」的区别,就像「请一个员工」和「打一次电话」的区别:员工有岗位、有工牌、会查资料、能记住上下文;电话打完了就挂了。
AgentScope 2.0 是阿里巴巴开源的多智能体框架,它最大的优点,是把「编排」这件事拆成了四个清晰的小积木。你只要按顺序把它们拼起来,就能得到任意复杂度的 Agent。今天这一篇,我们就拼最小、最基础的一层——让一个 Agent 开口说话。
三、四个积木:编排的最小单位
在 AgentScope 2.0 里,任何一个 Agent 都由 4 个零件拼成。先记住这张表,后面所有代码都是围绕它展开的:
| 零件 | 类名 | 它负责什么 | 打个比方 |
|---|---|---|---|
| 凭证 | Credential | 装 API Key,证明「你是谁」 | 门禁卡 |
| 模型 | Model | 真正的大模型大脑 | 发动机 |
| 智能体 | Agent | 名字 + 人设 + 大脑的合体 | 员工 |
| 消息 | Msg | 一条带角色标记的对话 | 传话的纸条 |
一句话概括这段编排逻辑:
凭证给模型「身份」,模型给 Agent「大脑」,人设给 Agent「性格」,消息给 Agent「输入」。
拆开看每一层:
- 凭证为什么单独拎出来?因为 Key 是敏感信息,单独封装后,你可以在框架里统一管理、统一脱敏,而不是让 Key 散落在代码各处。
- 模型被抽成对象后,DeepSeek、混元、百炼在你手里就变成了「同一套接口」。今天用 A 明天换 B,只改一行配置,业务逻辑一行不动。
- Agent 的
system_prompt(人设)决定了它的性格:同样的大脑,写出不同的人设,就是冷冰冰的工具人和有温度的服务者之间的差别。 - 消息带
role(角色)标记,是因为多轮对话里必须区分「用户说的」和「模型说的」,否则模型会分不清自己刚才说了啥。
理解了这四层,之后你要加工具(Function Calling)、加知识库(RAG)、加多 Agent 协作,全是在这四层之上继续往上搭。地基,就是这一篇。
四、运行环境(务必先对齐)
这篇文章的代码在下面的环境里验证过,建议保持一致,能省掉 90% 的奇怪报错:
- 系统:WSL Ubuntu-24.04(Windows 自带的 Linux 子系统)
- Python:3.11+,使用虚拟环境
venv隔离依赖 - 框架:AgentScope 2.x
- API Key:三把,DeepSeek / 混元 / 百炼,全部用环境变量注入
# 1. 进入 WSL 并激活虚拟环境
wsl -d Ubuntu-24.04
cd /2026_Study/agentscope/
source venv/bin/activate
# 2. 安装 AgentScope(已装可跳过)
pip install agentscope
# 3. 验证安装成功(能打印版本号即 OK)
python -c "import agentscope; print(agentscope.__version__)"
# 4. 把三把 Key 写进环境变量(sk-xxxx 换成你自己的真实 Key)
export DEEPSEEK_API_KEY="sk-xxxx"
export HUNYUAN_API_KEY="sk-xxxx"
export BAILIAN_API_KEY="sk-xxxx"
# 5. 运行本篇代码(代码文件见下方,存为 001_min_agent.py)
python 001_min_agent.py
一句话提醒:Key 只放在环境变量或
~/.bashrc里,永远不要写进代码、更不要提交到 Git。这既是安全习惯,也是后续所有文章的铁律。
五、完整代码(单文件自包含)
下面这份代码可以直接复制保存为 001_min_agent.py,单独运行。核心就是三件事:建模型 → 建 Agent → 发消息。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AgentScope 2.0 · Agent 编排初体验
把 4 个零件(凭证 / 模型 / 人设 / 消息)装配成一个能对话的智能体。
运行环境:WSL Ubuntu-24.04 + Python 3.11+ + AgentScope 2.x
运行命令:python 001_min_agent.py
"""
import os
import asyncio
import gc
# ============ 导入 AgentScope 的 4 个核心零件 ============
from agentscope.credential import (
DeepSeekCredential,
OpenAICredential,
DashScopeCredential,
)
from agentscope.model import (
DeepSeekChatModel,
OpenAIChatModel,
DashScopeChatModel,
)
from agentscope.agent import Agent
from agentscope.message import Msg
def build_models():
"""零件 1「凭证 Credential」:把「凭证 + 模型名」编排成 3 个模型对象。
凭证对象里装着 API Key,框架靠它去调大模型接口。
注意:Key 一律从环境变量读,绝不写死在代码里。
"""
# DeepSeek:官方原生凭证类,模型名已从 deepseek-chat 更新为 deepseek-v4-flash
deepseek = DeepSeekChatModel(
credential=DeepSeekCredential(api_key=os.environ["DEEPSEEK_API_KEY"]),
model="deepseek-v4-flash",
stream=False, # 关闭流式,一次拿完整回复,方便初学者处理
)
# 混元:兼容 OpenAI 格式,所以要显式给 base_url
hunyuan = OpenAIChatModel(
credential=OpenAICredential(
api_key=os.environ["HUNYUAN_API_KEY"],
base_url="https://tokenhub.tencentmaas.com/v1",
),
model="hy3",
stream=False, # 关闭流式,避免流式响应残留导致退出报错
)
# 百炼(阿里 DashScope):用官方 DashScope 凭证类
bailian = DashScopeChatModel(
credential=DashScopeCredential(api_key=os.environ["BAILIAN_API_KEY"]),
model="qwen-max",
stream=False, # 关闭流式,避免流式响应残留导致退出报错
)
return {"DeepSeek": deepseek, "混元": hunyuan, "百炼": bailian}
def extract_text(response):
"""从回复里取出纯文本。
AgentScope 的回复是「分块(block)」结构,一段回复可能拆成多个块。
这里逐块判断,遇到 text 块就取它的文本。
"""
for block in response.content:
if getattr(block, "type", None) == "text":
return block.text
return ""
async def run_agent(model, model_name, question):
"""零件 2/3/4:把「模型 + 人设」编排成 Agent,再编排一条消息,跑一次对话。
参数:
model —— 零件 2「模型 Model」,Agent 的「大脑」
model_name —— 展示用的模型名字
question —— 要问的问题
"""
# 零件 3「Agent」= 名字(name) + 人设(system_prompt) + 大脑(model)
agent = Agent(
name="小秘",
model=model,
system_prompt="你叫「小秘」,是一位亲切、耐心的技术学习助手。"
"回答简洁、清晰、有温度。",
)
# 零件 4「消息 Msg」:一条带角色标记的用户消息
msg = Msg(
name="user",
content=[{"type": "text", "text": question}],
role="user",
)
# 对话:把消息交给 Agent,等待回复(AgentScope 2.x 是 async 异步的)
response = await agent.reply(msg)
reply_text = extract_text(response)
# 顺便把 Token 用量捞出来,看看一次对话"烧"了多少
usage = getattr(response, "usage", None)
in_tok = getattr(usage, "input_tokens", "?") if usage else "?"
out_tok = getattr(usage, "output_tokens", "?") if usage else "?"
print(f"\n{'=' * 52}")
print(f" 模型:{model_name}")
print(f"{'=' * 52}")
print(f"🤖 小秘:{reply_text}")
print(f"📊 Token:in={in_tok} | out={out_tok}")
return reply_text
async def close_models(models):
"""收尾:显式关闭三个模型底层的 HTTP 连接池。
为什么要这一步:
三个模型(DeepSeek / 混元 / 百炼)底层全都持有一个 openai.AsyncClient,
存在 self.client 属性里;它内部又是 httpx → httpcore2 的异步连接池。
asyncio.run() 结束后事件循环被关闭,如果这些连接池还没被 await close(),
解释器退出时 GC 回收它们,异步生成器找不到活着的循环,就会抛
"generator didn't stop after athrow()" 的收尾报错。
所以趁循环还活着,把三个 client 干净关掉(注意 close() 是异步方法,要 await)。
"""
closed = 0
for m in models.values():
client = getattr(m, "client", None)
if client is None:
continue
try:
# openai.AsyncClient 的 close() 是异步方法,必须 await 才会真正关闭
await client.close()
closed += 1
except Exception:
# 关闭过程中的异常与本任务无关,忽略即可
pass
if closed:
print(f"\n🧹 已关闭 {closed} 个 HTTP 连接池")
def check_keys():
"""开工前先确认三把 Key 都在环境变量里,少了就友好提示。"""
need = ["DEEPSEEK_API_KEY", "HUNYUAN_API_KEY", "BAILIAN_API_KEY"]
missing = [k for k in need if not os.environ.get(k)]
if missing:
print("❌ 缺少环境变量:" + ", ".join(missing))
print(" 请先 export 对应的 API Key(参考文章「运行环境」一节)")
return False
return True
async def main():
# 先检查 Key,缺失就直接退出,避免跑到一半报 KeyError
if not check_keys():
return
models = build_models()
try:
# 初体验:先让 DeepSeek 跑通第一个 Agent
await run_agent(
models["DeepSeek"],
"DeepSeek (deepseek-v4-flash)",
"你好,请用一句话介绍你自己,再用一句话说明 AgentScope 是什么。",
)
# 进阶:同一个 Agent 骨架,换三个"大脑"各跑一遍
question = "请用一句话说说:什么是多智能体编排?"
for name, m in models.items():
await run_agent(m, name, question)
finally:
# 无论成功还是异常,都在事件循环关闭前清理连接池
await close_models(models)
# 断开模型引用并强制 GC,避免连接池在循环关闭后才被回收
del models
gc.collect()
if __name__ == "__main__":
asyncio.run(main())
六、代码拆解:三行看透编排
代码看着长,其实真正的「编排」只有三个动作,其余都是注释和善后:
① 建模型 —— build_models() 里,每个模型对象 = 一个凭证 + 一个模型名。凭证里的 Key 从 os.environ 读,这就是「门禁卡 + 发动机」的装配。三把 Key 对应三个不同厂商,但装完以后,它们在你手里是同一套接口——这就是编排的第一个好处:换模型只换凭证,不换逻辑。
② 建 Agent —— run_agent() 里,Agent(name, model, system_prompt) 一行,把名字、大脑、人设绑在一起。system_prompt 就是那个「性格」,它决定了同一个模型是冷冰冰的工具人,还是亲切的「小秘」。
③ 发消息 —— Msg(name, content, role) 造一条用户消息,await agent.reply(msg) 送出去,拿到 response。注意 await:AgentScope 2.x 是异步框架,调用处都要 async/await,主入口用 asyncio.run(main()) 兜底。
有两个小知识点值得单独说:
- 为什么用
async/await? 因为调大模型本质是「发一个网络请求,然后等它慢慢生成」。如果同步等,程序就卡在那里干耗;异步能让你同时发好几个请求、谁先回来先处理谁。对新手,你只需要记住套路:函数前加async,调用处加await,最后用asyncio.run(main())启动。 - 回复为什么要「分块」处理?
response.content是一个列表,里面每一块(block)都有自己的类型。文本块是text,未来还会有图片块、工具调用块。所以提取文字要遍历判断类型,而不是直接当字符串用。
最后那坨 close_models() 是收尾善后,负责把三个模型底层的 HTTP 连接池关干净——它不是编排的一部分,但缺了它程序退出时会报一个很唬人的错误,下一篇细讲。
七、运行结果(真实输出)
下面是这篇代码在 WSL Ubuntu-24.04 环境里的真实运行结果(模型有随机性,你的输出措辞会略有不同,属正常现象)。
第一次对话,DeepSeek 的自我介绍:
🤖 小秘:你好,我是小秘,你的贴心技术学习助手,随时帮你解答问题、理清思路。
AgentScope 是一个面向多智能体应用开发的 Python 框架,让构建、调试和部署多个 AI 代理变得简单高效。
📊 Token:in=116 | out=50
同一个问题「请用一句话说说:什么是多智能体编排?」,换三个大脑各跑一遍:
| 模型 | 输入/输出 Token | 回答 |
|---|---|---|
| DeepSeek(deepseek-v4-flash) | in=111 / out=36 | 多智能体编排就是像导演一样,把多个 AI「助手」分工、协调、串联起来,让它们各司其职、协同完成一个复杂任务的过程。 |
| 混元(hy3) | in=122 / out=24 | 多智能体编排就是统一协调多个 AI 智能体分工协作,像指挥小团队一样完成复杂任务。 |
| 百炼(qwen-max) | in=128 / out=25 | 多智能体编排是指协调多个智能体按照预定的流程或规则协同工作,以完成特定任务的过程。 |
最后一行 🧹 已关闭 3 个 HTTP 连接池,说明 close_models() 正常生效,程序干净退出、没有任何收尾报错。
这段真实输出能读出三件事:
- 同一个骨架,换模型就像换发动机。三个模型对「多智能体编排」的定义方向一致,但表达风格截然不同——DeepSeek 用「导演」打比方、混元用「指挥小团队」、百炼最书面克制。选谁,取决于你想要哪种「性格」。
- 三个厂商的凭证 + 模型,是同一套接口。DeepSeek(原生类)、混元(OpenAI 兼容)、百炼(DashScope)三种完全不同的接入方式,装进同一个
Agent后,调用逻辑一行不变——这就是「编排」的价值。 - 这种「一句话解释」类问题很省 Token,单次输出只有 24~50 个 Token。这也是为什么后续要研究「按场景选模型」和「省 Token」的技巧——Token 就是成本。
八、三个新手必踩的坑
坑 1:deepseek-chat 报模型不存在。 DeepSeek 的模型名已经更新为 deepseek-v4-flash,旧的 deepseek-chat 会被官方停用。看到 model not found 这类报错,先核对官网最新模型名,别在旧教程里死磕。
坑 2:generator didn't stop after athrow() 收尾报错。 模型底层的 HTTP 连接没关干净,不影响功能但很吓人。这个坑藏了三个雷:① 三个模型底层用的都是 openai.AsyncClient(存在 self.client 属性里,不是 httpx.AsyncClient);② 默认 stream=True 时,流式响应没被完整消费会残留一个 httpcore2 流生成器,等事件循环关闭后才被回收就报错;③ close() 是异步方法,必须 await。对应解法是代码里的两处:三个模型统一 stream=False 根治流残留 + close_models() 里 await client.close() 关连接池。这个坑我第一天连踩三次,三跑三错,代码注释里写的就是现场排查记录——像这种「查半天资料没人讲」的坑,后面系列里我专门开一篇做完整根因复盘。
坑 3:百炼的 Key 变量名陷阱。 ~/.bashrc 里可能写的是 DASHSCOPE_API_KEY,而代码读的是 BAILIAN_API_KEY。两者是同一把 Key,只是名字不同。运行前先 echo ${BAILIAN_API_KEY:+✅} 确认,缺了就重新 export 或做一次映射。
九、总结与下一步
这一篇,你用四行积木拼出了人生第一个 Agent:
Credential(门禁卡)→ Model(发动机)→ Agent(员工)→ Msg(纸条)→ reply
「编排」的本质,就是把易变的(模型、人设、消息)和稳定的(调用逻辑)拆开,让你能自由替换零件而不改骨架。这套思想会贯穿整个系列——后面无论加 RAG、加工具、加多 Agent,你都会发现:地基没变,只是往上多垒了几层。
下一篇《AgentScope 2.0 学习笔记:多轮对话,让 Agent 记住你》,我们会解决今天留下的悬念——agent.reply() 其实是无状态的,怎么让它「记住」上一句说过的话。敬请期待。
十、写在最后:卡住了怎么办
这篇真正跑起来,最耗时的往往不是代码本身,而是环境:Key 配错、模型名过期、收尾报错,每一个都够新手折腾半天的。坑 2 那种报错,网上搜得到的解释大多过时或不完整——这也是我决定把这个系列写下来的原因之一:踩过的坑,不想让你再踩一遍。
如果你在跑的过程中卡在两小时以上还没出来,别硬扛。把报错信息整理好,贴在评论区,我会帮你定位问题。如果你正在做 AI 相关的项目,无论是想给业务加个智能体、还是想做多智能体协作的架构设计,也欢迎来聊聊——这类工程问题,光靠教程解决不了,需要对着真实场景一个个抠细节。
下一篇见。
本文为「AgentScope 2.0 学习笔记」系列第 001 篇,代码已通过 py_compile 语法校验,运行环境见第四节。