docs: improve ch01 and ch02 quickstart documentation

- Add clear warnings for code snippets that cannot run directly
- Replace absolute paths with GitHub URLs
- Add natural transitions between Component and Agent concepts
- Add ChatModel vs ChatModelAgent comparison table
- Clarify the relationship between Component and Agent
- Add analogies for better understanding
- Improve language clarity and specificity
- Add ch02 example code for ChatModelAgent and Runner

Change-Id: I08e8944a7a09b65023b691e3c158a3b7f3fe5649
drew/english
shentong.martin 2 months ago committed by shentongmartin
parent b035dd29c5
commit cdedfd0425

@ -25,9 +25,10 @@ import (
"os"
"strings"
"github.com/cloudwego/eino-ext/components/model/ark"
"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
examplemodel "github.com/cloudwego/eino-examples/adk/common/model"
)
func main() {
@ -42,49 +43,53 @@ func main() {
}
ctx := context.Background()
cm := examplemodel.NewChatModel()
msgs := make([]*schema.Message, 0, 2)
if strings.TrimSpace(instruction) != "" {
msgs = append(msgs, schema.SystemMessage(instruction))
}
msgs = append(msgs, schema.UserMessage(query))
_, _ = fmt.Fprint(os.Stdout, "[assistant] ")
stream, err := cm.Stream(ctx, msgs)
if err == nil {
stream.SetAutomaticClose()
if err := printChatModelStream(stream); err != nil {
cm, err := newChatModel(ctx)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
_, _ = fmt.Fprintln(os.Stdout)
return
messages := []*schema.Message{
schema.SystemMessage(instruction),
schema.UserMessage(query),
}
out, err := cm.Generate(ctx, msgs)
_, _ = fmt.Fprint(os.Stdout, "[assistant] ")
stream, err := cm.Stream(ctx, messages)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if out != nil {
_, _ = fmt.Fprintln(os.Stdout, out.Content)
} else {
_, _ = fmt.Fprintln(os.Stdout)
}
}
defer stream.Close()
func printChatModelStream(stream *schema.StreamReader[*schema.Message]) error {
for {
frame, err := stream.Recv()
if errors.Is(err, io.EOF) {
return nil
break
}
if err != nil {
return err
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if frame != nil {
_, _ = fmt.Fprint(os.Stdout, frame.Content)
}
}
_, _ = fmt.Fprintln(os.Stdout)
}
func newChatModel(ctx context.Context) (model.ToolCallingChatModel, error) {
if os.Getenv("MODEL_TYPE") == "ark" {
return ark.NewChatModel(ctx, &ark.ChatModelConfig{
APIKey: os.Getenv("ARK_API_KEY"),
Model: os.Getenv("ARK_MODEL"),
BaseURL: os.Getenv("ARK_BASE_URL"),
})
}
return openai.NewChatModel(ctx, &openai.ChatModelConfig{
APIKey: os.Getenv("OPENAI_API_KEY"),
Model: os.Getenv("OPENAI_MODEL"),
BaseURL: os.Getenv("OPENAI_BASE_URL"),
ByAzure: os.Getenv("OPENAI_BY_AZURE") == "true",
})
}

@ -0,0 +1,118 @@
package main
import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/schema"
examplemodel "github.com/cloudwego/eino-examples/adk/common/model"
)
func main() {
var instruction string
flag.StringVar(&instruction, "instruction", "You are a helpful assistant.", "")
flag.Parse()
ctx := context.Background()
cm := examplemodel.NewChatModel()
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "Ch02ChatModelAgent",
Description: "A minimal ChatModelAgent with in-memory multi-turn history.",
Instruction: instruction,
Model: cm,
})
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: agent,
EnableStreaming: true,
})
history := make([]*schema.Message, 0, 16)
scanner := bufio.NewScanner(os.Stdin)
for {
_, _ = fmt.Fprint(os.Stdout, "you> ")
if !scanner.Scan() {
break
}
line := strings.TrimSpace(scanner.Text())
if line == "" {
break
}
history = append(history, schema.UserMessage(line))
events := runner.Run(ctx, history)
content, err := printAndCollectAssistantFromEvents(events)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
history = append(history, schema.AssistantMessage(content, nil))
}
if err := scanner.Err(); err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func printAndCollectAssistantFromEvents(events *adk.AsyncIterator[*adk.AgentEvent]) (string, error) {
var sb strings.Builder
for {
event, ok := events.Next()
if !ok {
break
}
if event.Err != nil {
return "", event.Err
}
if event.Output == nil || event.Output.MessageOutput == nil {
continue
}
mv := event.Output.MessageOutput
if mv.Role != schema.Assistant {
continue
}
if mv.IsStreaming {
mv.MessageStream.SetAutomaticClose()
for {
frame, err := mv.MessageStream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return "", err
}
if frame != nil && frame.Content != "" {
sb.WriteString(frame.Content)
_, _ = fmt.Fprint(os.Stdout, frame.Content)
}
}
_, _ = fmt.Fprintln(os.Stdout)
continue
}
if mv.Message != nil {
sb.WriteString(mv.Message.Content)
_, _ = fmt.Fprintln(os.Stdout, mv.Message.Content)
} else {
_, _ = fmt.Fprintln(os.Stdout)
}
}
return sb.String(), nil
}

@ -2,82 +2,138 @@
title: "第一章ChatModel 与 MessageConsole"
---
本章目标:用最小代码调用一次 ChatModel支持流式输出并掌握 `schema.Message` 的基本用法。
本章目标:理解 Eino 的 Component 抽象,用最小代码调用一次 ChatModel支持流式输出并掌握 `schema.Message` 的基本用法。
## 代码位置
- 入口代码:[cmd/ch01/main.go](file:///Users/bytedance/github/eino/examples/quickstart/chatwithdoc/cmd/ch01/main.go)
- 入口代码:[cmd/ch01/main.go](https://github.com/cloudwego/eino/blob/main/examples/quickstart/chatwithdoc/cmd/ch01/main.go)
## 前置条件
## 为什么需要 Component 接口
- Go 版本:与本目录 `go.mod` 一致
- 一个可调用的 ChatModel默认使用 OpenAI也支持 Ark
Eino 定义了一组 Component 接口(`ChatModel`、`Tool`、`Retriever`、`Loader` 等),每个接口描述一类可替换的能力:
本 quickstart 复用 `github.com/cloudwego/eino-examples/adk/common/model.NewChatModel()` 来创建模型,它会读取环境变量选择不同 provider
```go
type BaseChatModel interface {
Generate(ctx context.Context, input []*schema.Message, opts ...Option) (*schema.Message, error)
Stream(ctx context.Context, input []*schema.Message, opts ...Option) (
*schema.StreamReader[*schema.Message], error)
}
```
### 方式 AOpenAI默认
**接口带来的好处:**
需要配置(最小集合):
1. **实现可替换**`eino-ext` 提供了 OpenAI、Ark、Claude、Ollama 等多种实现,业务代码只依赖接口,切换模型只需改构造逻辑。
2. **编排可组合**Agent、Graph、Chain 等编排层只依赖 Component 接口,不关心具体实现。你可以把 OpenAI 换成 Ark编排代码无需改动。
3. **测试可 Mock**:接口天然支持 mock单元测试不需要真实调用模型。
- `OPENAI_API_KEY`
- `OPENAI_MODEL`
本章只涉及 `ChatModel`,后续章节会逐步引入 `Tool`、`Retriever` 等 Component。
可选:
## schema.Message对话的基本单位
- `OPENAI_BASE_URL`(例如走代理或兼容 OpenAI 协议的服务)
- `OPENAI_BY_AZURE=true`(如使用 Azure OpenAI
`Message` 是 Eino 里对话数据的基本结构:
示例:
```go
type Message struct {
Role RoleType // system / user / assistant / tool
Content string // 文本内容
ToolCalls []ToolCall // 仅 assistant 消息可能有
// ...
}
```
```bash
export OPENAI_API_KEY="..."
export OPENAI_MODEL="gpt-4.1-mini"
常用构造函数:
```go
schema.SystemMessage("You are a helpful assistant.")
schema.UserMessage("What is the weather today?")
schema.AssistantMessage("I don't know.", nil) // 第二个参数是 ToolCalls
schema.ToolMessage("tool result", "call_id")
```
### 方式 BArk
**角色语义:**
- `system`:系统指令,通常放在 messages 最前面
- `user`:用户输入
- `assistant`:模型回复
- `tool`:工具调用结果(后续章节涉及)
需要配置:
## 前置条件
- `MODEL_TYPE=ark`
- `ARK_API_KEY`
- `ARK_MODEL`
- Go 版本:与本目录 `go.mod` 一致
- 一个可调用的 ChatModel默认使用 OpenAI也支持 Ark
可选:
### 方式 AOpenAI默认
- `ARK_BASE_URL`
```bash
export OPENAI_API_KEY="..."
export OPENAI_MODEL="gpt-4.1-mini"
# 可选:
# OPENAI_BASE_URL代理或兼容服务
# OPENAI_BY_AZURE=true使用 Azure OpenAI
```
示例:
### 方式 BArk
```bash
export MODEL_TYPE="ark"
export ARK_API_KEY="..."
export ARK_MODEL="..."
# 可选ARK_BASE_URL
```
## 关键概念(只讲本章用到的)
- `schema.Message`:对话消息。常见角色为 `system/user/assistant/tool`。本章使用 `system + user` 作为输入。
- `ChatModel`:模型组件,负责基于一组 messages 生成回复。示例中用 `model.NewChatModel()` 创建。
## 运行
本章里我们用 Console 直接运行,不启动 Web 服务。
`examples/quickstart/chatwithdoc` 目录下执行:
```bash
cd /Users/bytedance/github/eino/examples/quickstart/chatwithdoc
go run ./cmd/ch01 -- "用一句话解释 Eino 的 Component 设计解决了什么问题?"
```
你会看到类似输出(流式逐步打印):
输出示例(流式逐步打印):
```text
[assistant] ...
[assistant] Eino 的 Component 设计通过定义统一接口...
```
## 入口代码做了什么
按执行顺序:
1. `NewChatModel()` 创建 ChatModel从环境变量选择 OpenAI/Ark
2. 构造输入 messages`SystemMessage(instruction)` + `UserMessage(query)`
3. 优先尝试 `ChatModel.Stream(...)` 并打印流式输出;若不支持则回退到 `ChatModel.Generate(...)`
1. **创建 ChatModel**:根据 `MODEL_TYPE` 环境变量选择 OpenAI 或 Ark 实现
2. **构造输入 messages**`SystemMessage(instruction)` + `UserMessage(query)`
3. **调用 Stream**:所有 ChatModel 实现都必须支持 `Stream()`,返回 `StreamReader[*Message]`
4. **打印结果**:迭代 `StreamReader` 逐帧打印 assistant 回复
关键代码片段(**注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [cmd/ch01/main.go](https://github.com/cloudwego/eino/blob/main/examples/quickstart/chatwithdoc/cmd/ch01/main.go)
```go
// 构造输入
messages := []*schema.Message{
schema.SystemMessage(instruction),
schema.UserMessage(query),
}
// 调用 Stream所有 ChatModel 都必须实现)
stream, err := cm.Stream(ctx, messages)
if err != nil {
log.Fatal(err)
}
defer stream.Close()
for {
chunk, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Print(chunk.Content)
}
```
## 本章小结
- **Component 接口**:定义可替换、可组合、可测试的能力边界
- **Message**:对话数据的基本单位,通过角色区分语义
- **ChatModel**:最基础的 Component提供 `Generate``Stream` 两个核心方法
- **实现选择**:通过环境变量或配置切换 OpenAI/Ark 等不同实现,业务代码无需改动

@ -0,0 +1,283 @@
---
title: "第二章ChatModelAgent、Runner、AgentEventConsole 多轮)"
---
本章目标:引入 ADK 的执行抽象Agent + Runner并用一个 Console 程序实现多轮对话。
## 代码位置
- 入口代码:[cmd/ch02/main.go](https://github.com/cloudwego/eino/blob/main/examples/quickstart/chatwithdoc/cmd/ch02/main.go)
## 前置条件
与第一章一致:需要配置一个可用的 ChatModelOpenAI 或 Ark
## 运行
`examples/quickstart/chatwithdoc` 目录下执行:
```bash
go run ./cmd/ch02
```
看到提示后输入问题(空行退出):
```text
you> 你好,解释一下 Eino 里的 Agent 是什么?
...
you> 再用一句话总结一下
...
```
## 关键概念
### 从 Component 到 Agent
第一章我们学习了 **Component**(组件),它是 Eino 中可替换、可组合的能力单元:
- `ChatModel`:调用大语言模型
- `Tool`:执行特定任务
- `Retriever`:检索信息
- `Loader`:加载数据
**Component 和 Agent 的关系:**
- **Component 不构成完整的 AI 应用**:它只是能力单元,需要被组织、编排、执行
- **Agent 是完整的 AI 应用**:它封装了完整的业务逻辑,可以直接运行
- **Agent 内部使用 Component**:最核心的是 `ChatModel`(对话能力)和 `Tool`(执行能力)
**为什么需要 Agent**
如果只有 Component你需要自己
- 管理对话历史
- 编排调用流程(何时调用模型、何时调用工具)
- 处理流式输出
- 实现中断恢复
- ...
**Agent 提供了什么?**
- **完整的运行时框架**:通过 `Runner` 统一管理执行过程
- **标准的事件流输出**`Run() -> AsyncIterator[*AgentEvent]`,支持流式、中断、恢复
- **可扩展能力**:可以添加 tools、middleware、interrupt 等
- **开箱即用**:创建 Agent 后直接运行,无需关心内部细节
**本章示例:**
`ChatModelAgent` 是最简单的 Agent它内部只使用了 `ChatModel`,但已经具备了 Agent 的完整能力框架。后续章节会展示如何添加 `Tool` 等更多能力。
### Agent 接口
`Agent` 是 ADK 中的核心接口,定义了智能体的基本行为:
```go
type Agent interface {
Name(ctx context.Context) string
Description(ctx context.Context) string
// Run 执行 Agent返回事件流
Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent]
}
```
**接口职责:**
- `Name()` / `Description()`:标识 Agent 的名称和描述
- `Run()`:执行 Agent 的核心方法,接收输入消息,返回事件流
**设计理念:**
- **统一抽象**:所有 AgentChatModelAgent、WorkflowAgent、SupervisorAgent 等)都实现这个接口
- **事件驱动**:通过事件流(`AsyncIterator[*AgentEvent]`)输出执行过程,支持流式响应
- **可扩展性**:后续加入 tools、middleware、interrupt 等能力时,接口保持不变
### ChatModelAgent
`ChatModelAgent` 是 Agent 接口的一个实现,基于 ChatModel 构建:
```go
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "Ch02ChatModelAgent",
Description: "A minimal ChatModelAgent with in-memory multi-turn history.",
Instruction: instruction,
Model: cm,
})
```
**ChatModel vs ChatModelAgent本质区别**
| 维度 | ChatModel | ChatModelAgent |
|------|-----------|----------------|
| **定位** | Component组件 | Agent智能体 |
| **接口** | `Generate() / Stream()` | `Run() -> AsyncIterator[*AgentEvent]` |
| **输出** | 直接返回消息内容 | 返回事件流(包含消息、控制动作等) |
| **能力** | 单纯的模型调用 | 可扩展 tools、middleware、interrupt 等 |
| **适用场景** | 简单的对话场景 | 复杂的智能体应用 |
**为什么需要 ChatModelAgent**
1. **统一抽象**ChatModel 只是 Component 的一种,而 Agent 是更高层的抽象,可以组合多种 Component
2. **事件驱动**Agent 输出事件流,支持流式响应、中断恢复、状态转移等复杂场景
3. **可扩展性**ChatModelAgent 可以添加 tools、middleware、interrupt 等能力,而 ChatModel 只能调用模型
4. **编排友好**Agent 可以被 Runner 统一管理,支持 checkpoint、恢复等运行时能力
**简单来说:**
- **ChatModel** = "负责与大语言模型通信的组件屏蔽不同模型提供商的差异OpenAI、Ark、Claude 等)"
- **ChatModelAgent** = "基于模型构建的智能体,可以调用模型,但还能做更多事"
**类比理解:**
- **ChatModel** 就像"数据库驱动":负责与数据库通信,屏蔽 MySQL/PostgreSQL 的差异
- **ChatModelAgent** 就像"业务逻辑层":基于数据库驱动构建,但还包含业务规则、事务管理等
**特点:**
- 封装了 ChatModel 的调用逻辑
- 提供统一的 `Run() -> AgentEvent` 输出形态
- 后续可以添加 tools、middleware 等能力
### Runner
`Runner` 是执行 Agent 的入口点,负责管理 Agent 的生命周期:
```go
type Runner struct {
a Agent // 要执行的 Agent
enableStreaming bool
store CheckPointStore // 用于中断恢复的状态存储
}
```
**为什么需要 Runner**
虽然 Agent 提供了 `Run()` 方法,但直接调用会缺少很多运行时能力:
1. **生命周期管理**Runner 管理 Agent 的启动、恢复、中断等状态
2. **Checkpoint 支持**:配合 `CheckPointStore` 实现中断恢复(后续章节涉及)
3. **统一入口**:提供 `Run()``Query()` 等便捷方法
4. **事件流封装**:将 Agent 的事件流转换为可消费的 `AsyncIterator[*AgentEvent]`
**使用方式:**
```go
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: agent,
EnableStreaming: true,
})
// 方式 1传入消息列表
events := runner.Run(ctx, history)
// 方式 2便捷方法传入单个查询字符串
events := runner.Query(ctx, "你好")
```
### AgentEvent
`AgentEvent` 是 Runner 返回的事件单元:
```go
type AgentEvent struct {
AgentName string
RunPath []RunStep
Output *AgentOutput // 输出内容
Action *AgentAction // 控制动作
Err error // 执行错误
}
```
**主要字段:**
- `event.Err`:执行错误
- `event.Output.MessageOutput`message 或 message stream流式
- `event.Action`:中断/转移/退出等控制动作(后续章节用到)
**消费方式:**
```go
for {
event, ok := events.Next()
if !ok {
break
}
if event.Err != nil {
// 处理错误
}
if event.Output != nil && event.Output.MessageOutput != nil {
// 处理消息输出(可能是流式)
}
}
```
## 多轮对话的实现
本章实现的是简单的多轮对话:用户输入 → 模型回复 → 用户继续输入 → ...
**实现方式:**
没有 tools 时,`ChatModelAgent` 在一次 `Run()` 里只会完成一轮模型调用。多轮对话是通过调用侧维护 history 实现的:
1. 用 `history []*schema.Message` 保存累计对话
2. 每次用户输入:把 `UserMessage` 追加到 history
3. 调用 `runner.Run(ctx, history)` 得到事件流,消费得到 assistant 文本
4. 把本轮 assistant 文本追加回 history进入下一轮
**关键代码片段(**注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [cmd/ch02/main.go](https://github.com/cloudwego/eino/blob/main/examples/quickstart/chatwithdoc/cmd/ch02/main.go)
```go
history := make([]*schema.Message, 0, 16)
for {
// 1. 读取用户输入
line := readUserInput()
if line == "" {
break
}
// 2. 追加用户消息到 history
history = append(history, schema.UserMessage(line))
// 3. 调用 Runner 执行 Agent
events := runner.Run(ctx, history)
// 4. 消费事件流,收集 assistant 回复
content := collectAssistantFromEvents(events)
// 5. 追加 assistant 消息到 history
history = append(history, schema.AssistantMessage(content, nil))
}
```
**流程图:**
```
┌─────────────────────────────────────────┐
│ 初始化 history = [] │
└─────────────────────────────────────────┘
┌──────────────────────┐
│ 用户输入 UserMessage │
└──────────────────────┘
┌──────────────────────┐
│ 追加到 history │
└──────────────────────┘
┌──────────────────────┐
│ runner.Run(history) │
└──────────────────────┘
┌──────────────────────┐
│ 消费事件流 │
└──────────────────────┘
┌──────────────────────┐
│ 追加 AssistantMessage│
└──────────────────────┘
(循环继续)
```
## 本章小结
- **Agent 接口**:定义智能体的基本行为,核心是 `Run() -> AsyncIterator[*AgentEvent]`
- **ChatModelAgent**:基于 ChatModel 实现的 Agent提供统一的执行抽象
- **Runner**Agent 的执行入口管理生命周期、checkpoint、事件流等运行时能力
- **AgentEvent**:事件驱动的输出单元,支持流式响应和控制动作
- **多轮对话**:通过调用侧维护 history 实现,每次 `Run()` 完成一轮对话
Loading…
Cancel
Save