AgentScope 2.0 学习笔记:Agent 编排初体验(零基础跑通第一个智能体)

5 阅读15分钟

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,只改一行配置,业务逻辑一行不动。
  • Agentsystem_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() 正常生效,程序干净退出、没有任何收尾报错。

这段真实输出能读出三件事:

  1. 同一个骨架,换模型就像换发动机。三个模型对「多智能体编排」的定义方向一致,但表达风格截然不同——DeepSeek 用「导演」打比方、混元用「指挥小团队」、百炼最书面克制。选谁,取决于你想要哪种「性格」。
  2. 三个厂商的凭证 + 模型,是同一套接口。DeepSeek(原生类)、混元(OpenAI 兼容)、百炼(DashScope)三种完全不同的接入方式,装进同一个 Agent 后,调用逻辑一行不变——这就是「编排」的价值。
  3. 这种「一句话解释」类问题很省 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 语法校验,运行环境见第四节。