Skip to content

第 3 章 · 运行 Agent 与 RunOutput 解析

本章目标:掌握 run()/arun()/print_response() 三种运行方式的区别,学会从 RunOutput 中取出内容、消息与指标,理解多轮对话的正确姿势。

3.1 三种运行方式

Agno 官方文档对运行方式的建议很明确:开发调试用 print_response(),生产代码用 run()arun()

python
# run_basics.py
import os
from dotenv import load_dotenv
from agno.agent import Agent
from agno.models.openai import OpenAIChat

load_dotenv()

agent = Agent(
    model=OpenAIChat(
        id="deepseek-chat",
        api_key=os.getenv("DEEPSEEK_API_KEY"),
        base_url="https://api.deepseek.com/v1",
    ),
)

# 方式一:开发时直接打印,人类可读,但拿不到返回值
agent.print_response("用一句话解释依赖注入")

# 方式二:同步运行,返回 RunOutput 对象——业务代码用这个
response = agent.run("用一句话解释闭包")
print(response.content)      # 真正的回答文本在这里

# 方式三:异步运行(协程),适合高并发服务端
import asyncio

async def main():
    resp = await agent.arun("用一句话解释装饰器")
    print(resp.content)

asyncio.run(main())

print_response 内部其实也调用了 run,只是额外负责格式化打印。写单元测试或 Web 接口时必须用 run/arun,因为你要对返回值做断言或加工。

3.2 RunOutput 核心字段

run() 的返回值是一个 RunOutput 对象,官方定义的核心属性如下:

字段含义
content最终回答内容(字符串;结构化输出时是 Pydantic 对象)
messages本次发送给模型的完整消息列表(含 system/工具消息)
metricstoken 用量、耗时等指标
run_id / session_id本次运行的唯一 ID 与所属会话 ID
agent_id / agent_name运行该次请求的 Agent 标识
reasoning_content思考型模型的推理过程文本
python
# inspect_output.py —— 把 RunOutput 拆开看
import os
from dotenv import load_dotenv
from agno.agent import Agent
from agno.models.openai import OpenAIChat

load_dotenv()
agent = Agent(model=OpenAIChat(
    id="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1",
))

r = agent.run("什么是幂等性?")

print("回答:", r.content[:80], "...")
print("run_id:", r.run_id)
print("消息数:", len(r.messages))
for m in r.messages:
    print("-", m.role, ":", str(m.content)[:50])
print("输入 tokens:", r.metrics.input_tokens, "/ 输出 tokens:", r.metrics.output_tokens)

metrics 有什么用

metrics 是成本核算的基础:把每次运行的 token 数记录下来,月底就能按用户/功能维度统计开销。第 23 章的可观测性会系统展开。

3.3 多轮对话:为什么第二次运行"失忆"了

先看一个新手必踩的坑:

python
# memory_trap.py —— 演示默认无记忆
agent = Agent(model=make_model())   # make_model 见 2.3 节写法

agent.run("我叫高小灵,最喜欢的数字是 7")
resp = agent.run("我的名字是什么?")   # ❌ 模型答不出来!
print(resp.content)

两次 run() 之间没有任何共享状态:每次调用都是独立的全新上下文,模型根本不知道上一次说过什么。解决办法有三种,复杂度递增:

  1. 手动拼接历史:自己把上一问一答加进下一次输入;
  2. chat history:让 Agno 自动携带同会话的历史运行(需配合存储,第 10–11 章);
  3. Memory:跨会话的用户级记忆(第 12 章)。

本节先用最朴素的手动方式打通概念:

python
# manual_history.py —— 手动维护对话历史
history = []

def chat(question: str) -> str:
    prompt = ""
    for q, a in history:                       # 把历史拼进提示词
        prompt += f"用户: {q}\n助手: {a}\n"
    prompt += f"用户: {question}"
    resp = agent.run(prompt)
    history.append((question, resp.content))   # 记录本轮结果
    return resp.content

chat("我最喜欢的颜色是蓝色")
print(chat("我喜欢什么颜色?"))                 # ✅ 能答出蓝色

这种写法的问题显而易见:提示词越来越长、token 成本线性上涨、无法持久化。所以它只适合理解原理,生产环境请使用第 10 章的会话机制。

3.4 异步并发运行多个 Agent

arun 的价值在并发场景才能体现:三个独立任务串行要等三次网络往返,异步并发只需最慢一次的时间:

python
# concurrent_runs.py —— asyncio.gather 并发三个请求
import asyncio
import os
from dotenv import load_dotenv
from agno.agent import Agent
from agno.models.openai import OpenAIChat

load_dotenv()
agent = Agent(model=OpenAIChat(
    id="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"),
)

async def main():
    questions = ["解释线程与进程的区别", "解释TCP与UDP的区别", "解释GET与POST的区别"]
    # 三个请求同时发出,gather 收集全部结果
    results = await asyncio.gather(*(agent.arun(q) for q in questions))
    for q, r in zip(questions, results):
        print(f"[{q}] -> {r.content[:40]}...")

asyncio.run(main())

注意 asyncio.gather 里的生成器表达式:为每个问题创建一个协程。如果某个请求失败会导致整个 gather 抛异常,生产中可用 return_exceptions=True 单独处理失败项。

3.5 本章小结

  • 开发调试用 print_response,业务代码用 run,并发场景用 arun
  • RunOutput 的关键信息:content 拿答案、messages 看上下文、metrics 算成本;
  • 默认情况下每次 run() 相互独立,模型没有跨调用记忆;
  • 手动拼历史能实现多轮但成本高,正确方案是会话存储 + chat history(第 10–11 章)。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 生产环境的 Web 服务中处理 Agent 请求,推荐使用哪种方式?

2. 连续两次 agent.run() 后,第二次提问涉及第一次的内容,模型却答不上来,原因是?

3. 想统计某次运行消耗了多少 token,应该访问 RunOutput 的哪个字段?

4. 关于 messages 字段,说法正确的是?

🛠️ 动手实践

  1. 编写脚本分别用 runarun 提同一个问题,打印两者的 run_idmetrics,确认它们是相互独立的运行。
  2. 扩展 3.3 的 manual_history.py:限制历史最多保留 3 轮,超出就丢弃最早的记录,观察回答质量变化。
  3. asyncio.gather 并发询问 5 个互不相关的问题,记录总耗时,再改为串行执行对比时间差。

掌握了同步运行后,进入第 4 章:流式输出与实时响应