【Eino 框架入门】用 Agent 实现多轮对话

【Eino 框架入门】用 Agent 实现多轮对话

上一篇用 ChatModel 调大模型,每次都是新对话,模型记不住你说过什么。这篇用 Agent + Runner 实现真正的多轮对话。

跟上一篇的区别

ChatModel (ch01) Agent + Runner (ch02)
定位 底层组件 上层封装
对话 每次独立 多轮连续
输出 StreamReader 事件流

打个比方:ChatModel 像直接发 HTTP 请求,Agent 像用一个封装好的 SDK,帮你处理了 system prompt、输出格式这些事。

核心代码

go 复制代码
// 1. 创建 Agent(封装 ChatModel)
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
    Instruction: "你是个助手",  // 自动变成 system prompt
    Model:       cm,
})

// 2. 创建 Runner
runner := adk.NewRunner(ctx, adk.RunnerConfig{
    Agent:           agent,
    EnableStreaming: true,
})

// 3. 多轮对话
history := []*schema.Message{}
for {
    history = append(history, schema.UserMessage(input))
    events := runner.Run(ctx, history)
    reply := collectReply(events)
    history = append(history, schema.AssistantMessage(reply, nil))
}

Agent 封装了什么

Agent 就是把 ChatModel 包了一层,帮你做几件事:

自动加 system promptInstruction 字段会自动变成 SystemMessage,不用自己构造。

统一输出格式。不管流式还是非流式,都走事件流,处理逻辑统一。

预留扩展点。以后加工具(Tool)、中间件,接口不用改。

go 复制代码
// 这两段代码等价:

// ch01 的写法
messages := []*schema.Message{
    schema.SystemMessage("你是个助手"),
    schema.UserMessage("你好"),
}
stream, _ := cm.Stream(ctx, messages)

// ch02 的写法
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
    Instruction: "你是个助手",
    Model:       cm,
})
// Run 的时候只要传 UserMessage,SystemMessage 自动加

Runner 是什么

Runner 是 Agent 的执行器。你给它 Agent 和输入,它跑起来,返回事件流。

为什么要 Runner 而不是直接调 Agent?因为 Runner 管了生命周期、状态、事件分发这些事,你不用关心。

go 复制代码
runner := adk.NewRunner(ctx, adk.RunnerConfig{
    Agent:           agent,
    EnableStreaming: true,  // 启用流式输出
})

events := runner.Run(ctx, history)  // 返回事件流

多轮对话的本质

Agent 本身不存历史。多轮对话的关键是 history,你每次 Run() 时把完整历史传进去:

go 复制代码
// 第一轮
history = [UserMessage("你好")]
// 模型回复 "你好!"

// 第二轮
history = [
    UserMessage("你好"),
    AssistantMessage("你好!", nil),
    UserMessage("我刚才说了什么?"),
]
// 模型能看到完整历史,回复 "你说了你好"

所以多轮对话的本质就是:每次调用都把历史带上。history 越来越长,模型看到的上下文就越来越多。

事件流怎么读

Runner 返回的是事件流,不是直接的文本。事件流就像一个管道,Agent 干完一件事就发个事件过来。

go 复制代码
events := runner.Run(ctx, history)

for {
    event, ok := events.Next()
    if !ok {
        break  // 流结束了
    }
    if event.Err != nil {
        // 出错了
    }

    mv := event.Output.MessageOutput
    if mv.Role != schema.Assistant {
        continue  // 跳过非 assistant 消息
    }

    if mv.IsStreaming {
        // 流式:逐帧读取
        for {
            frame, err := mv.MessageStream.Recv()
            if errors.Is(err, io.EOF) {
                break
            }
            fmt.Print(frame.Content)
        }
    } else {
        // 非流式:直接读
        fmt.Print(mv.Message.Content)
    }
}

为什么要事件流:以后加了工具调用,你也能从同一个流里收到工具事件,不用改处理逻辑。一个流,所有输出统一处理。

AgentEvent 里有什么

rust 复制代码
AgentEvent
├── Err              // 错误
└── Output
    └── MessageOutput
        ├── Role          // Assistant / User / System / Tool
        ├── IsStreaming   // 是否流式
        ├── Message       // 非流式时的完整消息
        └── MessageStream // 流式时的 StreamReader

以后还可能加 ToolCallAction 等字段,处理工具调用和控制指令。

什么时候用 Agent

简单对话用 ChatModel 够了。但如果你后续要:

  • 加工具调用(让模型能查数据库、调 API)
  • 加中断恢复(长时间任务中间停掉,下次继续)
  • 加监控追踪(看模型调了几次、花了多少钱)

就用 Agent,它预留了这些扩展点。

相关推荐
维克兜率天4 小时前
【维克】ARMA模型:AR与MA的完美结合
后端·restful
是未才5 小时前
从输入 URL 到页面返回:DNS、路由、TLS 与 HTTP 完整链路
java·后端·计算机网络
上海安当技术6 小时前
半天接入:USBKey RESTful API + C 动态库,Web 和 C/S 两套集成路径实战
前端·后端·restful·集成·usbkey
DevUI团队6 小时前
从“即兴创作”到“规格先行”,华为云码道(CodeArts)代码智能体持续深耕企业级规范驱动开发能力
前端·人工智能·后端
weixin_431600447 小时前
NestJS 入门(3):Guard 如何挡住未登录请求?
前端·后端·学习·nest.js
IT_陈寒8 小时前
JavaScript类型转换把我坑惨了,这破玩意真该早点搞明白
前端·人工智能·后端
用户938515635078 小时前
手写一个 LLM Harness 框架:用工程化手段把大模型幻觉踩在脚下
javascript·人工智能·后端
wei_shuo9 小时前
KES 云原生部署与弹性扩展:容器化、Kubernetes编排与自动伸缩
后端
一木之林9 小时前
Python.五.(一)--1. 并发编程、异步IO与多进程
后端
feng尘9 小时前
# 彻底搞懂 ReentrantLock 与 tryLock:从秒杀实战到 AQS 独占模式源码剖析
后端