# 第十章：Web 服务化与 SSE 流式传输

欢迎来到第十章！在前面的章节中，我们构建的 Agent 都运行在终端里——用户在 TUI 里输入，Agent 在本地执行。

本章的核心任务是**服务化**：把 Agent 的核心逻辑从 TUI 中剥离出来，封装成 HTTP 服务，让浏览器也能像 ChatGPT 一样与 Agent 流式对话。

> **核心设计理念**：
> - **Agent 与传输层解耦**：Agent 内部用 `StreamEvent` channel 输出事件，不感知任何 HTTP 概念；HTTP 层负责把事件转成 SSE 格式推给客户端
> - **树形消息结构**：每条消息记录 `parent_message_id`，支持分支对话；查询历史时沿祖先链向上追溯，拼接成完整的 LLM history

---

## 🎯 你将学到什么

1. **Agent 服务化**：如何把 CLI Agent 剥离 TUI，封装为 HTTP 服务
2. **SSE 流式传输**：Server-Sent Events 的服务端实现，以及前后端协议设计
3. **会话持久化**：Conversation / Message 数据模型设计，用 SQLite 存储对话历史
4. **树形对话历史**：通过 `parent_message_id` 支持分支对话，`buildHistory` 沿祖先链重建 LLM context

---

## 🛠 核心功能

### 1. Agent 与传输层解耦

**问题**：之前章节的 Agent 直接向 TUI channel 发送 `MessageVO`，耦合了展示层。如果要改成 HTTP 服务，需要修改 Agent 内部代码。

**解决方案**：定义 `StreamEvent`——一个与传输层无关的内部事件类型。Agent 只负责产生 `StreamEvent`，HTTP 层负责转换格式：

```
Agent.RunStreaming()
   ↓  chan StreamEvent（业务语义）
service.CreateMessage()
   ↓  toSSEMessage()
   ↓  chan SSEMessageVO（传输格式）
controller.createMessage()
   ↓  c.SSEvent() + Flush
   ↓  text/event-stream（HTTP 协议）
```

`StreamEvent` 只包含业务字段，不涉及 HTTP：

```go
type StreamEvent struct {
    Event            string  // reasoning / content / tool_call / tool_result / error
    Content          string
    ReasoningContent string
    ToolCall         string
    ToolArguments    string
    ToolResult       string
}
```

这样 Agent 核心逻辑完全不感知传输层，未来换成 WebSocket 也只需改 HTTP 层。

### 2. 树形消息结构与历史重建

**问题**：Web 服务需要支持多轮对话，但每次 HTTP 请求是无状态的。Agent 需要知道"之前聊了什么"才能继续对话。

**解决方案**：借鉴 ChatGPT 的设计，每条消息记录 `parent_message_id`，消息之间形成一棵树：

```
根消息（parent=""）
  └── 第2轮消息（parent=根消息ID）
        ├── 第3轮消息A（parent=第2轮ID）  ← 用户继续对话
        └── 第3轮消息B（parent=第2轮ID）  ← 用户重新生成
```

`buildHistory` 从 `parent_message_id` 出发，沿树向根追溯，将路径上每条消息的 `Rounds` 拼接成 LLM history：

```go
// 从 parentMessageID 向根节点追溯，收集路径（顺序：根 -> parent）
path := make([]*ChatMessage, 0)
cur := parentMessageID
for cur != "" {
    msg, ok := index[cur]
    if !ok { break }
    path = append(path, msg)
    cur = msg.ParentMessageID
}
// 反转后拼接每条消息的 rounds
```

### 3. Rounds 持久化

每次 `RunStreaming` 结束后，把本轮所有 LLM 消息（user + assistant + tool results）序列化为 JSON 存入 `ChatMessage.Rounds`：

```
一次对话轮次的 Rounds：
  [user消息, assistant消息（含tool_calls）, tool结果消息, assistant最终消息]
       ↓ json.Marshal
  存入 ChatMessage.Rounds 字段
       ↓ 下次对话时 json.Unmarshal
  恢复为 []OpenAIMessage，作为 history 传给 Agent
```

这样即使服务重启，历史对话也能完整恢复。

### 4. SSE 并发架构

每个请求的处理流程：

```
HTTP 请求进入
   ↓
创建 eventCh (chan SSEMessageVO)
   ↓
设置 SSE 响应头（text/event-stream）
   ↓
go func() {
    s.CreateMessage(ctx, ..., eventCh)  ← 后台运行 Agent
    close(eventCh)
}()
   ↓
for e := range eventCh {
    c.SSEvent("message", e)  ← 主协程写 SSE
    c.Writer.Flush()         ← 立即推送给客户端
}
```

客户端断开连接时，`ctx.Done()` 会触发，Agent loop 中的 `select` 检测到后提前退出，避免资源泄漏。

---

## 📖 代码结构速览

```
ch10/
├── agent/
│   ├── agent.go       # Agent 核心：RunStreaming 接受 history，通过 eventCh 输出 StreamEvent
│   ├── stream.go      # StreamEvent 类型定义（与传输层解耦）
│   └── tool/
│       ├── tool.go    # Tool 接口定义
│       └── bash.go    # Bash 工具实现
├── server/
│   ├── db.go          # GORM 数据模型（Conversation、ChatMessage）+ InitDB
│   ├── history.go     # buildHistory：沿 parent_message_id 树追溯，拼接 LLM history
│   ├── service.go     # 业务层：CRUD + 启动 Agent 流式执行 + toSSEMessage 转换
│   └── controller.go  # HTTP 路由 + SSE 控制器（Gin）
├── vo/
│   ├── vo.go          # 请求/响应 VO：CreateConversationReq、ChatMessageVO 等
│   └── sse.go         # SSEMessageVO：SSE 事件的 JSON 格式
└── main/
    └── main.go        # 入口：初始化 DB、Agent、Server，监听 :8080
```

### 数据模型

```go
type Conversation struct {
    ConversationID string  // UUID
    UserID         string
    Title          string
    CreatedAt      int64
}

type ChatMessage struct {
    MessageID       string  // UUID
    ConversationID  string
    ParentMessageID string  // 树形结构的关键字段
    Query           string  // 用户原始提问
    Response        string  // 模型最终输出
    Rounds          string  // 本轮所有 LLM 消息，JSON 序列化
    Model           string
    Usage           string  // token 使用量，JSON 序列化
    CreatedAt       int64
}
```

---

## 💡 使用示例

### 运行服务

```bash
cp .env.example .env
cp config.example.json config.json
# 编辑 config.json，填入 LLM 配置

go run ./ch10/main
# 服务监听 :8080，数据库文件 ch10.db 自动创建
```

### API 调用示例

```bash
# 1. 创建会话
curl -s -X POST http://localhost:8080/api/conversation \
  -H 'Content-Type: application/json' \
  -d '{"user_id":"alice","title":"我的第一个对话"}' | jq .

# 响应：
# {"code":0,"msg":"ok","data":{"conversation_id":"uuid-xxx",...}}

# 2. 发送消息，观察 SSE 流式输出（替换 {conversation_id}）
curl -N -X POST http://localhost:8080/api/conversation/{conversation_id}/message \
  -H 'Content-Type: application/json' \
  -d '{"user_id":"alice","query":"用 Go 写一个冒泡排序"}'

# 流式输出：
# data: {"message_id":"...","event":"content","content":"好的"}
# data: {"message_id":"...","event":"content","content":"，我来"}
# data: {"message_id":"...","event":"tool_call","tool_call":"bash","tool_arguments":"{...}"}
# data: {"message_id":"...","event":"tool_result","tool_call":"bash","tool_result":"..."}
# data: {"message_id":"...","event":"content","content":"..."}

# 3. 继续对话（传入 parent_message_id 保持上下文）
curl -N -X POST http://localhost:8080/api/conversation/{conversation_id}/message \
  -H 'Content-Type: application/json' \
  -d '{"user_id":"alice","query":"再加上注释","parent_message_id":"{message_id}"}'

# 4. 查询历史消息
curl -s http://localhost:8080/api/conversation/{conversation_id}/message | jq .
```

### SSE 事件格式

每个 SSE 事件的 `data` 字段是一个 JSON 对象：

| `event` 值 | 含义 | 有效字段 |
|-----------|------|---------|
| `reasoning` | 推理模型的思考内容（流式） | `reasoning_content` |
| `content` | 模型文本输出片段（流式） | `content` |
| `tool_call` | 发起工具调用 | `tool_call`（工具名）、`tool_arguments` |
| `tool_result` | 工具执行结果 | `tool_call`（工具名）、`tool_result` |
| `error` | 错误信息 | `content` |

---

## 🖥 前端

`frontend/` 目录包含一个配套的 React + TypeScript 前端，用于直观展示 SSE 流式对话效果，技术栈为 Vite + React + Tailwind CSS。

前端实现了：侧边栏会话列表、流式消息气泡（含推理内容折叠展示）、工具调用折叠面板。

```bash
cd frontend
pnpm install
pnpm dev   # 开发模式，自动代理 /api 到 localhost:8080
```

前端通过 `@microsoft/fetch-event-source` 消费 SSE 流，代理配置在 `vite.config.ts` 中：

```ts
server: {
  proxy: { '/api': 'http://localhost:8080' }
}
```

---

## 🔧 API 接口一览

| 方法 | 路径 | 说明 |
|------|------|------|
| `POST` | `/api/conversation` | 创建会话 |
| `GET` | `/api/conversation?user_id=` | 列出会话 |
| `POST` | `/api/conversation/:id/message` | 发送消息（SSE 流式响应） |
| `GET` | `/api/conversation/:id/message` | 查询消息历史 |

---

## 与前几章的关系

ch10 在 ch09 Agent 能力的基础上，专注解决**服务化**这一工程问题。为保持代码清晰，ch10 有意简化了 Agent（去掉了 TUI、上下文管理、记忆、RAG、技能等高级特性），只保留最核心的 Agent Loop + bash 工具。

后续章节将在 ch10 的服务化基础上，逐步叠加状态管理、评测等能力。
