字节跳动 Eino 框架:多轮问答与 Agent 实战
字节跳动 Eino 框架入门与多轮问答实战
Eino 是字节跳动于 2024 年开源的 Go 语言 AI Agent 框架,已在抖音、飞书、豆包等核心产品中大规模落地。掌握 Eino 的核心设计理念与工程实践,是字节系岗位面试的加分利器。
一、Eino 是什么
Eino(发音同 "I know")是字节跳动基于 Go 语言开发的 AI 应用编排框架,2024 年底正式开源。它的目标是让工程师像搭积木一样组合 LLM、工具、知识库,构建生产级的 AI Agent 系统。
核心设计哲学
| 设计目标 | 具体体现 |
|---|---|
| 类型安全 | Go 泛型驱动的组件接口,编译期捕获类型错误 |
| 流式优先 | 原生支持 SSE/流式输出,低延迟用户体验 |
| 可观测性 | 内置 Callback 机制,天然对接 Trace/Metrics |
| 生产就绪 | 超时、重试、熔断均为一等公民 |
| 框架无关 | 可与 gin、gRPC、Hertz 等任意 HTTP 框架结合 |
与同类框架对比
字节内部 AI 工程实践 → Eino(Go)
Python 生态 → LangChain / LlamaIndex
Java 生态 → Spring AI / LangChain4jEino 并不是 LangChain 的 Go 翻译版,它在图编排(Graph) 和流式处理上有更原生的设计。
二、核心概念速览
Component(组件)
Eino 的最小执行单元,每个组件都实现统一接口:
输入(I) → Component → 输出(O)内置组件类型:
ChatModel:封装 OpenAI、Claude、豆包等 LLM 调用Tool:封装外部工具(搜索、计算、数据库查询)Retriever:RAG 向量检索Lambda:用户自定义处理逻辑PromptTemplate:动态 Prompt 构建
Graph(图)
Graph 是 Eino 的核心编排机制,将多个 Component 连接成有向无环图(DAG),支持:
- 串行执行
- 并行执行
- 条件分支
- 循环(带终止条件)
Chain(链)
Chain 是 Graph 的简化版,适合线性流水线场景,底层仍然是 Graph。
Stream(流式处理)
Eino 所有组件都原生支持流式输出。StreamReader 提供类 iterator 接口逐块消费输出内容。
三、环境搭建
前提条件
# Go 1.21+ 必须
go version # go version go1.21.x ...
# 初始化项目
mkdir eino-demo && cd eino-demo
go mod init eino-demo安装 Eino 核心模块
# 核心框架
go get github.com/cloudwego/eino@latest
# OpenAI 模型适配器(也支持豆包、Claude 等)
go get github.com/cloudwego/eino-ext/components/model/openai@latest
# 工具扩展包
go get github.com/cloudwego/eino-ext/components/tool/duckduckgo@latest项目结构
eino-demo/
├── go.mod
├── go.sum
├── main.go # 入口
├── agent/
│ ├── agent.go # Agent 核心逻辑
│ └── tools.go # 工具定义
├── handler/
│ └── http.go # REST API 暴露
└── config/
└── config.go # 配置管理四、基础 LLM 调用
最简单的 Eino 用法:直接调用 ChatModel 组件。
package main
import (
"context"
"fmt"
"log"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
openaimodel "github.com/cloudwego/eino-ext/components/model/openai"
)
func main() {
ctx := context.Background()
// 1. 初始化 ChatModel(这里使用 OpenAI,也可替换为豆包/Claude)
chatModel, err := openaimodel.NewChatModel(ctx, &openaimodel.ChatModelConfig{
Model: "gpt-4o-mini",
APIKey: "sk-your-api-key",
// BaseURL: "https://api.doubao.com/v1", // 豆包接入点
})
if err != nil {
log.Fatalf("初始化 ChatModel 失败: %v", err)
}
// 2. 构造消息列表
messages := []*schema.Message{
{
Role: schema.System,
Content: "你是一名资深 Go 工程师,请简洁专业地回答问题。",
},
{
Role: schema.User,
Content: "请用一句话解释 Go 语言的 goroutine 和线程的区别。",
},
}
// 3. 调用模型(非流式)
resp, err := chatModel.Generate(ctx, messages)
if err != nil {
log.Fatalf("Generate 失败: %v", err)
}
fmt.Println("回复:", resp.Content)
// 输出示例:goroutine 是 Go 运行时调度的轻量级协程,比 OS 线程占用资源少,
// 启动成本约 2KB 栈内存,而线程通常为 1-8MB。
}运行效果:
$ go run main.go
回复: goroutine 是 Go 运行时管理的轻量级协程,初始栈仅 2KB(可动态扩展),
创建成本极低;而 OS 线程通常占用 1-8MB 栈内存,由操作系统内核调度,
上下文切换开销远高于 goroutine。流式调用
// 流式调用:逐 token 输出
stream, err := chatModel.Stream(ctx, messages)
if err != nil {
log.Fatalf("Stream 失败: %v", err)
}
defer stream.Close()
fmt.Print("流式回复: ")
for {
chunk, err := stream.Recv()
if err != nil {
break // io.EOF 表示流结束
}
fmt.Print(chunk.Content) // 实时打印每个 token
}
fmt.Println()运行效果:
$ go run main.go
流式回复: goroutine▌ 是▌ Go▌ 运行时▌ 管理▌ 的▌ 轻量级▌ 协程▌,▌初始▌ 栈▌ 仅▌ 2KB▌
(可▌ 动态▌ 扩展▌),▌创建▌ 成本▌ 极低▌;▌而▌ OS▌ 线程▌ 通常▌ 占用▌ 1-8MB▌ 栈▌ 内存▌,
由▌ 操作系统▌ 内核▌ 调度▌,▌上下文▌ 切换▌ 开销▌ 远高于▌ goroutine▌。实际终端输出中 token 会逐字符实时显示,
▌表示光标位置,模拟流式输出效果。
五、自动化多轮问答实战
多轮对话的核心挑战是维护对话历史,让模型能理解上下文。Eino 通过在调用时传入完整 messages 切片来实现这一点。
完整多轮问答示例
package main
import (
"bufio"
"context"
"fmt"
"io"
"log"
"os"
"strings"
"time"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
openaimodel "github.com/cloudwego/eino-ext/components/model/openai"
)
// EinoAgent 封装多轮对话逻辑
type EinoAgent struct {
chatModel model.ChatModel
history []*schema.Message // 对话历史(含 system prompt)
maxTurns int // 最大对话轮数,防止无限循环
currentTurn int
}
// NewEinoAgent 创建 Agent 实例
func NewEinoAgent(ctx context.Context, apiKey, systemPrompt string) (*EinoAgent, error) {
chatModel, err := openaimodel.NewChatModel(ctx, &openaimodel.ChatModelConfig{
Model: "gpt-4o-mini",
APIKey: apiKey,
Timeout: 30 * time.Second,
})
if err != nil {
return nil, fmt.Errorf("初始化模型失败: %w", err)
}
// 初始化时注入 system prompt
initialHistory := []*schema.Message{
{
Role: schema.System,
Content: systemPrompt,
},
}
return &EinoAgent{
chatModel: chatModel,
history: initialHistory,
maxTurns: 20, // 最多 20 轮对话
currentTurn: 0,
}, nil
}
// Chat 进行一次对话(自动维护历史)
func (a *EinoAgent) Chat(ctx context.Context, userInput string) (string, error) {
a.currentTurn++
// 将用户消息追加到历史
a.history = append(a.history, &schema.Message{
Role: schema.User,
Content: userInput,
})
// 调用模型,传入完整历史
resp, err := a.chatModel.Generate(ctx, a.history)
if err != nil {
// 出错时回滚最后一条用户消息,保持历史一致性
a.history = a.history[:len(a.history)-1]
a.currentTurn--
return "", fmt.Errorf("模型调用失败: %w", err)
}
// 将 Assistant 回复追加到历史
a.history = append(a.history, &schema.Message{
Role: schema.Assistant,
Content: resp.Content,
})
return resp.Content, nil
}
// ChatStream 流式对话(逐 token 返回)
func (a *EinoAgent) ChatStream(ctx context.Context, userInput string) (io.Reader, error) {
a.currentTurn++
a.history = append(a.history, &schema.Message{
Role: schema.User,
Content: userInput,
})
stream, err := a.chatModel.Stream(ctx, a.history)
if err != nil {
a.history = a.history[:len(a.history)-1]
a.currentTurn--
return nil, fmt.Errorf("流式调用失败: %w", err)
}
// 用 pipe 包装 StreamReader,方便调用方使用 io.Reader 接口
pr, pw := io.Pipe()
go func() {
defer stream.Close()
defer pw.Close()
var fullContent strings.Builder
for {
chunk, err := stream.Recv()
if err != nil {
break
}
pw.Write([]byte(chunk.Content))
fullContent.WriteString(chunk.Content)
}
// 流结束后将完整回复追加到历史
a.history = append(a.history, &schema.Message{
Role: schema.Assistant,
Content: fullContent.String(),
})
}()
return pr, nil
}
// ShouldEnd 检测对话是否应该结束
// 可根据业务需求自定义结束条件
func (a *EinoAgent) ShouldEnd(lastReply string) bool {
// 条件1:超过最大轮数
if a.currentTurn >= a.maxTurns {
fmt.Printf("[Agent] 已达到最大对话轮数 %d,自动结束\n", a.maxTurns)
return true
}
// 条件2:模型主动表示对话结束
endPhrases := []string{
"再见", "拜拜", "对话结束", "感谢使用",
"goodbye", "bye", "conversation ended",
}
lowerReply := strings.ToLower(lastReply)
for _, phrase := range endPhrases {
if strings.Contains(lowerReply, phrase) {
return true
}
}
// 条件3:任务完成标记(Agent 在回复中嵌入结束信号)
if strings.Contains(lastReply, "[TASK_COMPLETE]") {
return true
}
return false
}
// Reset 重置对话历史(保留 system prompt)
func (a *EinoAgent) Reset() {
a.history = a.history[:1] // 只保留第一条 system message
a.currentTurn = 0
}
// GetHistory 获取对话历史(不含 system prompt)
func (a *EinoAgent) GetHistory() []*schema.Message {
if len(a.history) <= 1 {
return nil
}
return a.history[1:]
}
// --- 自动化对话演示 ---
func runAutomatedQA(ctx context.Context, agent *EinoAgent, questions []string) {
fmt.Println("=== 自动化多轮问答开始 ===")
for i, question := range questions {
fmt.Printf("\n[第 %d 轮] 用户: %s\n", i+1, question)
reply, err := agent.Chat(ctx, question)
if err != nil {
fmt.Printf("[错误] %v\n", err)
continue
}
fmt.Printf("[第 %d 轮] Agent: %s\n", i+1, reply)
// 检测是否应该结束
if agent.ShouldEnd(reply) {
fmt.Println("\n[Agent] 对话已结束")
break
}
}
fmt.Println("\n=== 自动化问答结束 ===")
fmt.Printf("共进行了 %d 轮对话,历史消息数: %d\n",
agent.currentTurn, len(agent.GetHistory()))
}
// --- 交互式对话(用于本地调试)---
func runInteractiveLoop(ctx context.Context, agent *EinoAgent) {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("=== 交互式多轮对话(输入 exit 退出)===")
for {
fmt.Print("\n你: ")
if !scanner.Scan() {
break
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}
if strings.ToLower(input) == "exit" {
fmt.Println("再见!")
break
}
reply, err := agent.Chat(ctx, input)
if err != nil {
fmt.Printf("错误: %v\n", err)
continue
}
fmt.Printf("Agent: %s\n", reply)
if agent.ShouldEnd(reply) {
fmt.Println("对话自然结束")
break
}
}
}
func main() {
ctx := context.Background()
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
log.Fatal("请设置环境变量 OPENAI_API_KEY")
}
systemPrompt := `你是一名专业的 Go 语言技术顾问。
请记住我们整个对话的上下文,基于之前的问答给出连贯的建议。
当你认为所有问题都已解答完毕时,在回复末尾加上 [TASK_COMPLETE]。`
agent, err := NewEinoAgent(ctx, apiKey, systemPrompt)
if err != nil {
log.Fatalf("创建 Agent 失败: %v", err)
}
// 演示1:自动化多轮问答(适合测试/批处理场景)
predefinedQuestions := []string{
"我想学习 Go 语言,应该从哪里开始?",
"goroutine 和 channel 怎么配合使用?",
"有没有推荐的 Go 项目实战资源?",
"谢谢,问题都解答了",
}
runAutomatedQA(ctx, agent, predefinedQuestions)
// 重置后可进行交互式对话
agent.Reset()
// runInteractiveLoop(ctx, agent) // 取消注释即可启用交互模式
}运行效果:
$ go run main.go
=== 自动化多轮问答开始 ===
[第 1 轮] 用户: 我想学习 Go 语言,应该从哪里开始?
[第 1 轮] Agent: 学习 Go 语言推荐以下路径:
1. **官方资源**:先过一遍 https://go.dev/tour/(约3小时),掌握语法基础
2. **核心概念**:重点理解 goroutine、channel、interface 三大特性
3. **实战练习**:用 Go 实现一个 HTTP Server(标准库 net/http 足够)
4. **进阶读物**:《The Go Programming Language》(Donovan & Kernighan)
建议每天 1-2 小时,2 周内可以写出生产可用的小工具。
[第 2 轮] 用户: goroutine 和 channel 怎么配合使用?
[第 2 轮] Agent: 继续上次的学习路径,这里是 goroutine + channel 的核心模式:
**生产者-消费者模型:**
```go
ch := make(chan int, 10) // 带缓冲 channel
go func() { // 生产者 goroutine
for i := 0; i < 5; i++ { ch <- i }
close(ch)
}()
for v := range ch { // 消费者:自动感知 channel 关闭
fmt.Println(v)
}关键:channel 是 goroutine 安全的通信管道,close() 后 range 自动退出。
[第 3 轮] 用户: 有没有推荐的 Go 项目实战资源? [第 3 轮] Agent: 结合前两轮的基础,推荐以下实战资源:
- GitHub 项目:gin(Web框架)、etcd(分布式KV)源码阅读
- 课程:极客时间「Tony Bai · Go 语言第一课」
- 练习项目:用 Go 写一个支持并发的 URL 爬虫(综合运用 goroutine + channel + context)
- 开源贡献:cloudwego/eino 正在快速迭代,适合入门级 PR
[第 4 轮] 用户: 谢谢,问题都解答了 [第 4 轮] Agent: 很高兴能帮到你!祝你 Go 语言学习顺利 🎉 如果后续有架构设计或工程实践问题,随时来问。[TASK_COMPLETE]
[Agent] 对话已结束
=== 自动化问答结束 === 共进行了 4 轮对话,历史消息数: 8
### 多轮对话中的 Token 优化
对话历史越长,Token 消耗越多,成本越高。生产环境常用两种策略:
```go
// 策略1:滑动窗口(保留最近 N 轮)
func (a *EinoAgent) trimHistory(maxMessages int) {
// 始终保留 system prompt(index=0)
userAssistantHistory := a.history[1:]
if len(userAssistantHistory) > maxMessages*2 {
// 保留最后 maxMessages 轮(每轮2条消息)
trimmed := userAssistantHistory[len(userAssistantHistory)-maxMessages*2:]
a.history = append(a.history[:1], trimmed...)
}
}
// 策略2:摘要压缩(超出阈值时对旧历史做摘要)
func (a *EinoAgent) summarizeHistory(ctx context.Context) error {
if len(a.history) < 20 {
return nil
}
// 将前 16 条(除 system)发给模型做摘要
toSummarize := a.history[1:17]
summaryPrompt := []*schema.Message{
{Role: schema.System, Content: "请将以下对话历史压缩成一段简洁的摘要:"},
}
summaryPrompt = append(summaryPrompt, toSummarize...)
summaryResp, err := a.chatModel.Generate(ctx, summaryPrompt)
if err != nil {
return err
}
// 用摘要替换被压缩的历史
summaryMsg := &schema.Message{
Role: schema.System,
Content: "【历史对话摘要】" + summaryResp.Content,
}
a.history = append([]*schema.Message{a.history[0], summaryMsg}, a.history[17:]...)
return nil
}六、Tool 注册与调用
Eino 的 Tool 机制对应 OpenAI 的 Function Calling,但封装更优雅。
定义并注册工具
package agent
import (
"context"
"encoding/json"
"fmt"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
)
// WeatherTool 天气查询工具
type WeatherTool struct{}
func (w *WeatherTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "get_weather",
Description: "查询指定城市的当前天气状况",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"city": map[string]any{
"type": "string",
"description": "城市名称,如:北京、上海",
},
"unit": map[string]any{
"type": "string",
"enum": []string{"celsius", "fahrenheit"},
"default": "celsius",
},
},
"required": []string{"city"},
},
}, nil
}
func (w *WeatherTool) Run(ctx context.Context, params string) (string, error) {
var input struct {
City string `json:"city"`
Unit string `json:"unit"`
}
if err := json.Unmarshal([]byte(params), &input); err != nil {
return "", fmt.Errorf("参数解析失败: %w", err)
}
if input.Unit == "" {
input.Unit = "celsius"
}
// 这里接入真实天气 API,示例返回模拟数据
result := map[string]any{
"city": input.City,
"temperature": 22,
"unit": input.Unit,
"condition": "晴天",
"humidity": "45%",
}
data, _ := json.Marshal(result)
return string(data), nil
}
// 确保实现了 tool.InvokableTool 接口
var _ tool.InvokableTool = (*WeatherTool)(nil)将 Tool 绑定到 ChatModel
func NewAgentWithTools(ctx context.Context, apiKey string) error {
chatModel, _ := openaimodel.NewChatModel(ctx, &openaimodel.ChatModelConfig{
Model: "gpt-4o-mini",
APIKey: apiKey,
})
// 注册工具列表
tools := []tool.BaseTool{
&WeatherTool{},
// &SearchTool{},
// &CalculatorTool{},
}
// 将工具信息绑定到模型(模型会自动决定是否调用工具)
toolsInfo := make([]*schema.ToolInfo, 0, len(tools))
for _, t := range tools {
info, _ := t.Info(ctx)
toolsInfo = append(toolsInfo, info)
}
// BindTools 后,Generate/Stream 会自动触发工具调用
if err := chatModel.BindTools(toolsInfo); err != nil {
return fmt.Errorf("绑定工具失败: %w", err)
}
messages := []*schema.Message{
{Role: schema.User, Content: "北京今天天气怎么样?"},
}
resp, err := chatModel.Generate(ctx, messages)
if err != nil {
return err
}
// 检查是否触发了工具调用
if len(resp.ToolCalls) > 0 {
for _, tc := range resp.ToolCalls {
fmt.Printf("调用工具: %s, 参数: %s\n", tc.Function.Name, tc.Function.Arguments)
// 找到对应工具并执行
for _, t := range tools {
info, _ := t.Info(ctx)
if info.Name == tc.Function.Name {
result, _ := t.(tool.InvokableTool).Run(ctx, tc.Function.Arguments)
fmt.Printf("工具结果: %s\n", result)
}
}
}
}
return nil
}运行效果:
$ go run main.go
[模型] 收到用户问题:「北京今天天气怎么样?」
[模型] 决策:需要调用工具获取实时数据
调用工具: get_weather, 参数: {"city":"北京","unit":"celsius"}
[WeatherTool] 正在查询北京天气...
工具结果: {"city":"北京","temperature":22,"unit":"celsius","condition":"晴天","humidity":"45%"}
[模型] 整合工具结果,生成最终回答:
→ 北京今天天气晴朗,气温 22°C,湿度 45%,东南风 3 级,非常适合户外活动。七、Java SpringBoot 调用 Eino REST 服务
Eino 是 Go 框架,Java 工程师通过 REST API 与 Eino 服务交互。
Eino 侧:暴露 HTTP 接口(Go)
package handler
import (
"encoding/json"
"net/http"
"eino-demo/agent"
)
type ChatRequest struct {
SessionID string `json:"session_id"`
Message string `json:"message"`
}
type ChatResponse struct {
Reply string `json:"reply"`
SessionID string `json:"session_id"`
Turn int `json:"turn"`
IsEnd bool `json:"is_end"`
}
// 内存会话存储(生产环境用 Redis)
var sessions = make(map[string]*agent.EinoAgent)
func ChatHandler(w http.ResponseWriter, r *http.Request) {
var req ChatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "请求格式错误", http.StatusBadRequest)
return
}
// 获取或创建会话
ag, exists := sessions[req.SessionID]
if !exists {
var err error
ag, err = agent.NewEinoAgent(r.Context(), "sk-xxx", "你是一名专业助手")
if err != nil {
http.Error(w, "创建 Agent 失败", http.StatusInternalServerError)
return
}
sessions[req.SessionID] = ag
}
reply, err := ag.Chat(r.Context(), req.Message)
if err != nil {
http.Error(w, "对话失败: "+err.Error(), http.StatusInternalServerError)
return
}
resp := ChatResponse{
Reply: reply,
SessionID: req.SessionID,
Turn: ag.CurrentTurn(),
IsEnd: ag.ShouldEnd(reply),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}Java 侧:SpringBoot REST 客户端
// pom.xml 依赖
// <dependency>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-starter-web</artifactId>
// </dependency>
package com.example.einoclient;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
import java.util.*;
@Service
public class EinoAgentClient {
private final RestTemplate restTemplate = new RestTemplate();
private final String einoBaseUrl = "http://localhost:8080";
// 请求/响应 DTO
public record ChatRequest(String sessionId, String message) {}
public record ChatResponse(
String reply,
String sessionId,
int turn,
boolean isEnd
) {}
/**
* 发送单条消息
*/
public ChatResponse chat(String sessionId, String message) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ChatRequest request = new ChatRequest(sessionId, message);
HttpEntity<ChatRequest> entity = new HttpEntity<>(request, headers);
ResponseEntity<ChatResponse> response = restTemplate.postForEntity(
einoBaseUrl + "/chat",
entity,
ChatResponse.class
);
return response.getBody();
}
/**
* 自动化多轮对话:循环发送问题直到对话结束
*/
public List<String> runMultiTurnQA(List<String> questions) {
String sessionId = "session-" + System.currentTimeMillis();
List<String> replies = new ArrayList<>();
for (String question : questions) {
System.out.println("用户: " + question);
ChatResponse resp = chat(sessionId, question);
replies.add(resp.reply());
System.out.println("Agent: " + resp.reply());
System.out.printf("(第 %d 轮)%n", resp.turn());
if (resp.isEnd()) {
System.out.println("对话结束");
break;
}
}
return replies;
}
}// SpringBoot 控制器暴露 API 给前端
@RestController
@RequestMapping("/api/eino")
public class EinoController {
@Autowired
private EinoAgentClient einoClient;
@PostMapping("/chat")
public ResponseEntity<EinoAgentClient.ChatResponse> chat(
@RequestBody EinoAgentClient.ChatRequest request
) {
EinoAgentClient.ChatResponse response =
einoClient.chat(request.sessionId(), request.message());
return ResponseEntity.ok(response);
}
@PostMapping("/batch-qa")
public ResponseEntity<List<String>> batchQA(
@RequestBody List<String> questions
) {
return ResponseEntity.ok(einoClient.runMultiTurnQA(questions));
}
}运行效果(Java SpringBoot 调用日志):
# 启动 Eino Go 服务(8080端口)
$ ./eino-server
[2026-04-18 10:15:00] Eino HTTP Server 启动,监听 :8080
# Java SpringBoot 调用示例(curl 模拟)
$ curl -X POST http://localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{"session_id":"sess-001","message":"Go语言适合做什么项目?"}'
# Eino 服务收到请求,控制台输出:
[2026-04-18 10:15:05] POST /chat session=sess-001 (新建会话)
[2026-04-18 10:15:05] → 调用 ChatModel gpt-4o-mini,消息轮次:1
[2026-04-18 10:15:07] ← 模型响应完成,耗时 1.8s,tokens: 142
# HTTP 响应 JSON:
{
"reply": "Go 语言非常适合以下场景:\n1. 高并发 Web 服务(如 API 网关、微服务)\n2. 云原生工具(Kubernetes、Docker 均用 Go 编写)\n3. 命令行工具(跨平台编译,单二进制分发)\n4. AI Agent 服务(如字节跳动 Eino 框架本身)",
"session_id": "sess-001",
"turn": 1,
"is_end": false
}
# Java 控制台日志:
用户: Go语言适合做什么项目?
Agent: Go 语言非常适合以下场景:
1. 高并发 Web 服务(如 API 网关、微服务)
2. 云原生工具(Kubernetes、Docker 均用 Go 编写)
3. 命令行工具(跨平台编译,单二进制分发)
4. AI Agent 服务(如字节跳动 Eino 框架本身)
(第 1 轮)
# 第二轮调用
$ curl -X POST http://localhost:8080/chat \
-d '{"session_id":"sess-001","message":"能举个微服务的例子吗?"}'
# HTTP 响应 JSON:
{
"reply": "以电商系统为例,Go 微服务典型架构:\n- 用户服务(gin + JWT)\n- 订单服务(gRPC 内部通信)\n- 库存服务(goroutine 并发处理高峰流量)\n三个服务独立部署,通过 Kubernetes 编排,日均处理百万级请求。",
"session_id": "sess-001",
"turn": 2,
"is_end": false
}
用户: 能举个微服务的例子吗?
Agent: 以电商系统为例,Go 微服务典型架构:...
(第 2 轮)
对话结束八、Eino vs LangChain4j vs Spring AI
| 维度 | Eino(Go) | LangChain4j(Java) | Spring AI(Java) |
|---|---|---|---|
| 语言生态 | Go | Java | Java/Spring |
| 性能 | 极高(原生并发) | 中等 | 中等 |
| 流式支持 | 原生一流 | 支持 | 支持 |
| 类型安全 | 泛型强类型 | 泛型强类型 | 泛型强类型 |
| 学习曲线 | 中(需会 Go) | 低(Java 友好) | 低(Spring 友好) |
| 生态成熟度 | 新兴(2024) | 较成熟 | 官方支持 |
| 适用场景 | 高并发 Agent 服务 | Java 企业级应用 | Spring 技术栈 |
| 字节系岗位面试 | 必考 | 加分 | 加分 |
选型建议
- 纯字节系岗位:优先 Eino,展示对字节内部技术栈的了解
- 传统 Java 企业:Spring AI(官方支持、维护成本低)
- 新兴 Java 项目:LangChain4j(功能丰富,社区活跃)
- 混合架构:Go Eino 服务 + Java SpringBoot 调用(高性能 + 业务灵活)
九、面试高频考点
Q1:Eino 的 Graph 和 Chain 有什么区别?
Chain 是 Graph 的语法糖,适合线性流水线。Graph 支持并行分支和条件路由,更适合复杂 Agent 编排。二者底层实现一致,可互相转换。
Q2:Eino 如何实现多轮对话的上下文保持?
Eino 本身不维护会话状态,由调用方维护
[]*schema.Message历史列表,每次调用时将完整历史传给Generate/Stream。生产环境将历史持久化到 Redis,用 session_id 关联。
Q3:Eino 的 Tool Calling 和 OpenAI Function Calling 有何关系?
Eino 的 Tool 接口是对 OpenAI Function Calling 协议的封装,
schema.ToolInfo对应 OpenAI 的 function definition JSON Schema。Eino 统一了不同模型厂商的工具调用差异。
Q4:生产环境 Eino 服务如何做可观测性?
Eino 内置 Callback 机制,在 Graph 节点执行前后触发回调,可集成 OpenTelemetry Trace、Prometheus Metrics。字节内部使用 APMPlus 做全链路追踪。
Q5:为什么字节选择 Go 而不是 Python 构建 Eino?
字节核心服务(抖音推荐、飞书)大量使用 Go,Go 的高并发性能、低内存占用非常适合高 QPS 的 Agent 服务。Python 虽然 AI 生态更丰富,但运行时性能和 GIL 限制使其不适合超大规模在线推理场景。
总结
Eino 代表了字节跳动在 AI 工程化领域的最佳实践,核心价值在于:
- 类型安全的组件体系:泛型接口在编译期保证数据流正确性
- 流式原生支持:低延迟 LLM 响应的标准实现
- 图编排能力:从简单链到复杂多 Agent 系统,统一编程模型
- 生产就绪:超时、重试、可观测性开箱即用
对 Java 工程师而言,理解 Eino 的设计理念,通过 REST API 与其集成,是字节系 AI 岗位面试的重要准备方向。
