LLM大模型会话ID身份跟踪标识原理解构:从模型无状态下的会话ID(Session ID)原理分析以及自主实现会话跟踪

LLM大模型会话ID身份跟踪标识原理解构:从模型无状态下的会话ID(Session ID)原理分析以及自主实现会话跟踪

晚上和一个老同学相互交流学习到很晚,讨论持续到凌晨,关于在AI Agent应用实际开发过程中开发者都会面临的一个问题或者说是疑问:模型是无状态的也没有缓存“记忆”机制,那会话的上下文是怎么实现跟踪的呢?
带着这个疑问和思考,以下我将从设计原理到实践进行解构,帮理理解Agent开发中个最核心的问题之一。

其实很多开发者对大模型的会话ID唯一性标识充满了疑问和不解,要想从事Agent应用开发,首先要有个意识:

  • 1.大模型是无状态性的;
  • 2.大模型并不具有存储和记忆(具体表现为内部token处理完输出结果终止后释放,类似流水线)。注意这里指的是在一次推理(从输入 Prompt 到生成完整输出)完成后,本次处理过的 token 通常会被释放(即从显存/内存中清除),但具体取决于你是否需要保留对话上下文。

时机大模型使用过程中,人类不管是chat还是Agent方式交互,都是有状态性的对话跟踪,这个里面实现的原理,其实非常简单生硬,就是在模型上层实现会话的跟踪,接下来会进行详细结构。

首先是LLM无状态协议下的“记忆”挑战

在大语言模型(LLM)的应用开发中,我们面临着一个核心的矛盾:模型推理本质是无状态的(Stateless),而人类对话本质是有状态的(Stateful)

HTTP 协议作为 LLM 服务的主要载体,遵循请求 - 响应模式,服务器默认不保留任何客户端的上一次请求信息。然而,一个多轮对话应用(Chatbot)必须“记住”用户之前说了什么。解决这一矛盾的关键枢纽,就是 会话 ID(Session ID / Conversation ID)

本文将深入剖析主流部署工具(vLLM, Ollama, OpenAI, DeepSeek)的会话管理机制,揭示无状态模型实现上下文跟踪的技术原理,并最终使用 Golang 实现一套高并发、支持异步处理的专有会话 ID 管理方案。


第一部分:主流的部署工具的会话 ID 策略分析(参考相关源码和模型介绍)

不同的模型服务提供商和部署工具,对“会话”的理解和实现层级各不相同。理解这些差异是设计通用会话管理层的前提。
部署工具的好处是已经帮开发者实现了一套会话ID的生成跟踪机制,一般开发者只要获取会话ID进行跟踪和开发即可,原理本质上都一样,只是放在哪一层的策略问题。

根据各家模型的介绍和结合开源模型源码可以大概知晓其会话ID的基本实现方式:

1.1 OpenAI API (Chat Completion vs Assistants)

OpenAI 提供了两种主要模式,其会话管理逻辑截然不同:

  • Chat Completion API (/v1/chat/completions):
    • 机制: 完全无状态。
    • 会话 ID: API 本身不生成会话 ID。
    • 原理: 客户端必须在每次请求中携带完整的 messages 数组(包含历史对话)。会话跟踪完全由客户端或中间件负责。
  • Assistants API (/v1/threads):
    • 机制: 服务端有状态。
    • 会话 ID: 服务端生成 thread_id
    • 原理: 消息存储在服务端,客户端只需发送 thread_id 和新消息。

1.2 vLLM (推理引擎)

  • 机制: vLLM 是一个高性能推理引擎,专注于 Token 生成速度。
  • 会话 ID: 原生不支持
  • 原理: vLLM 暴露的 API 通常兼容 OpenAI 标准。它期望接收完整的 Prompt 或 History。会话管理通常由包裹 vLLM 的 Serving Layer(如 FastAPI, LangChain Serve)实现。

1.3 Ollama (本地部署)

  • 机制: 本地运行,支持流式输出。
  • 会话 ID: 客户端管理
  • 原理: Ollama 的 /api/chat 接口接受 messages 列表。虽然 Ollama 进程在内存中可能缓存部分 KV Cache 以加速同一连接的连续请求,但从 API 契约来看,它依赖客户端传递历史上下文。

1.4 DeepSeek / 云厂商模型

  • 机制: 类似 OpenAI Chat Completion。
  • 会话 ID: 部分云厂商在响应头或 Body 中返回 request_idconversation_id 用于日志追踪,但上下文记忆仍需客户端维护

1.5 架构对比图

graph TD
    subgraph Client ["客户端 / 中间件"]
        C1[维护 Session ID]
        C2[存储 History 上下文]
    end

    subgraph Provider ["模型服务层"]
        O1[OpenAI Chat: 无状态]
        O2[OpenAI Assistants: 有状态 Thread]
        V1[vLLM/Ollama: 无状态推理]
    end

    C1 -->|携带 Session ID + History| O1
    C1 -->|携带 Session ID + History| V1
    C1 -->|仅携带 Thread ID| O2
    
    style O1 fill:#f9f,stroke:#333
    style V1 fill:#f9f,stroke:#333
    style O2 fill:#bbf,stroke:#333

第二部分:无状态模型实现会话跟踪的技术原理

既然模型本身“健忘”,我们需要在应用层构建“海马体”。

2.1 核心原理:上下文窗口(Context Window)

LLM 的“记忆”实际上是输入 Token 的一部分

  1. 请求时:客户端将 历史消息 + 新消息 拼接成完整的 List 发送给模型。
  2. 响应时:模型基于整个 List 生成回复。
  3. 存储时:客户端将 新消息 + 模型回复 追加到本地存储的历史记录中。

2.2 会话 ID 的生成与生命周期

会话 ID 是对话的“主键”。

  • 生成方式:
    • UUID v4: 随机性强,适合分布式生成,无碰撞风险。
    • Snowflake: 有序 ID,适合数据库索引优化。
    • Hash: 基于用户 ID+ 时间戳生成,可重现但需防碰撞。
  • 存储介质:
    • Redis: 适合高频读写,设置 TTL 自动过期。
    • Database: 适合持久化归档。
    • In-Memory: 适合单节点临时测试(生产环境不推荐)。

2.3 会话跟踪流程图

sequenceDiagram
    participant User as 用户
    participant App as 应用服务 (Go)
    participant Store as 存储 (Redis/DB)
    participant LLM as 模型服务

    User->>App: 发送消息 (无 ID 或带旧 ID)
    
    alt 新会话
        App->>App: 生成新 Session ID (UUID)
    else 旧会话
        App->>Store: 根据 ID 加载历史上下文
    end
    
    App->>Store: 保存新 Session ID
    App->>App: 拼接历史 + 新消息
    App->>LLM: 请求补全 (携带完整 Context)
    LLM-->>App: 返回回复
    App->>Store: 更新历史 (追加回复)
    App-->>User: 返回回复 + Session ID

第三部分:异步处理中的会话 ID 逻辑

LLM 推理耗时较长(秒级到分钟级),在 Web 服务中通常采用异步任务模式。此时,会话 ID 不仅是“对话标识”,更是“任务关联标识”。

3.1 异步挑战

  1. 请求断开: HTTP 请求可能在模型返回前超时。
  2. 状态同步: 用户如何知道哪个回复属于哪个会话?
  3. 并发冲突: 同一会话在短时间内收到两条消息,如何保证上下文顺序?

3.2 解决方案:会话锁与任务队列

  • Correlation ID: 在 Session ID 基础上,为每次请求生成唯一的 Request ID,用于追踪单次任务。
  • 会话锁 (Session Lock): 防止同一 Session ID 的并发写入导致上下文错乱。
  • 回调机制: 通过 WebSocket 或 SSE (Server-Sent Events) 将结果推回客户端,携带原始 Session ID。

3.3 异步处理逻辑图

graph TD
    Req["HTTP 请求"] --> Gen["生成 Session ID & Task ID"]
    Gen --> Queue["推入任务队列"]
    Gen --> Ack["立即返回 202 Accepted + Session ID"]
    
    Worker["异步 Worker"] --> Queue
    Worker --> Lock["获取会话锁"]
    Lock --> Load["加载上下文"]
    Load --> Infer["调用 LLM"]
    Infer --> Save["保存新上下文"]
    Save --> Unlock["释放会话锁"]
    Save --> Notify["推送结果 (SSE/Webhook)"]
    
    style Gen fill:#ff9,stroke:#333
    style Lock fill:#f96,stroke:#333

第四部分:Golang 专有会话 ID 解决方案实战(仅作原理性理解)

从事开发工作这些年来,我一直有个习惯:如果不理解一个东西,那就亲自动手DIY一下尝试去实现它,在解决问题的过程中就能很快理解而且记忆深刻。只知其然而不知其所以然的结果就是很快淡忘

以下将使用 Golang 实现一个线程安全、支持上下文滑动窗口、并具备异步处理能力的会话管理中间件。

4.1 设计目标

  1. 唯一性: 基于 UUID 生成会话 ID(注意:实际ID的生成方案有很多,主流雪花算法等,只要确保原子性即可)。
  2. 线程安全: 支持高并发读写。
  3. 上下文管理: 自动维护消息历史,支持最大 Token 数截断。
  4. 异步友好: 提供锁机制防止并发污染。

4.2 核心数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package session

import (
"sync"
"time"

"github.com/google/uuid"
)

// Message 代表单条对话消息
type Message struct {
Role string `json:"role"` // system, user, assistant
Content string `json:"content"` // 内容
Time int64 `json:"time"` // 时间戳
}

// Session 代表一个完整的会话对象
type Session struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Messages []Message `json:"messages"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
MaxHistory int `json:"max_history"` // 最大保留消息条数 (滑动窗口)
mu sync.Mutex // 会话级锁,防止并发修改上下文
}

// Manager 会话管理器 (单例)
type Manager struct {
sessions map[string]*Session
mu sync.RWMutex // 管理器级锁,保护 map
}

// NewManager 初始化管理器
func NewManager() *Manager {
return &Manager{
sessions: make(map[string]*Session),
}
}

4.3 会话 ID 生成与获取逻辑

这里实现了“有则复用,无则新建”的逻辑,并加入了滑动窗口清理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// GetOrCreateSession 获取或创建会话
func (m *Manager) GetOrCreateSession(userID, sessionID string) *Session {
m.mu.Lock()
defer m.mu.Unlock()

if sessionID != "" {
if s, exists := m.sessions[sessionID]; exists {
return s
}
}

// 创建新会话
newID := uuid.New().String()
s := &Session{
ID: newID,
UserID: userID,
Messages: make([]Message, 0),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
MaxHistory: 20, // 默认保留最近 20 条
}
m.sessions[newID] = s
return s
}

// AddMessage 添加消息并维护上下文 (线程安全)
func (s *Session) AddMessage(role, content string) {
s.mu.Lock()
defer s.mu.Unlock()

msg := Message{
Role: role,
Content: content,
Time: time.Now().Unix(),
}

s.Messages = append(s.Messages, msg)
s.UpdatedAt = time.Now()

// 滑动窗口:如果超过最大限制,移除最早的消息
// 注意:实际生产中应基于 Token 数计算,此处简化为条数
if len(s.Messages) > s.MaxHistory {
s.Messages = s.Messages[len(s.Messages)-s.MaxHistory:]
}
}

// GetContext 获取用于发送给 LLM 的上下文
func (s *Session) GetContext() []Message {
s.mu.Lock()
defer s.mu.Unlock()

// 返回副本,防止外部修改
ctx := make([]Message, len(s.Messages))
copy(ctx, s.Messages)
return ctx
}

4.4 异步处理与锁机制实现

在异步场景下,必须确保同一个 Session ID 在同一时间只有一个请求在修改上下文,否则会出现“丢失消息”或“上下文错乱”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// AsyncTask 模拟异步任务结构
type AsyncTask struct {
SessionID string
UserMsg string
ResultChan chan string
}

// ProcessAsync 模拟异步处理入口
func (m *Manager) ProcessAsync(userID, sessionID, msg string) (string, chan string) {
session := m.GetOrCreateSession(userID, sessionID)
resultChan := make(chan string, 1)

task := AsyncTask{
SessionID: session.ID,
UserMsg: msg,
ResultChan: resultChan,
}

// 将任务放入全局队列 (此处简化为直接启动 goroutine)
go m.handleTask(task)

return session.ID, resultChan
}

// handleTask 核心异步逻辑
func (m *Manager) handleTask(task AsyncTask) {
// 1. 获取会话 (此时会话已存在)
m.mu.RLock()
session, exists := m.sessions[task.SessionID]
m.mu.RUnlock()

if !exists {
task.ResultChan <- "Error: Session not found"
close(task.ResultChan)
return
}

// 2. 锁定会话 (关键步骤:防止并发写)
// 注意:Session.AddMessage 内部已有锁,但我们需要保证
// "读取上下文 -> 调用 LLM -> 写入回复" 的原子性逻辑在业务层可控
// 这里简化演示,实际建议在外部加锁或使用乐观锁

// 用户消息先入库
session.AddMessage("user", task.UserMsg)

// 3. 模拟 LLM 调用 (耗时操作)
// 在实际代码中,这里会调用 vLLM/Ollama/OpenAI
// 需要传入 session.GetContext()
mockLLMResponse := "这是模型基于上下文的回复 (Async)"

// 4. 模型回复入库
session.AddMessage("assistant", mockLLMResponse)

// 5. 通知结果
task.ResultChan <- mockLLMResponse
close(task.ResultChan)
}

4.5 完整调用示例 (Main)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"your_project/session" // 假设上面的代码在 session 包
)

func main() {
manager := session.NewManager()

// 第一次请求 (创建会话)
sid, ch := manager.ProcessAsync("user_123", "", "你好,介绍一下你自己")
fmt.Printf("新会话 ID: %s\n", sid)

// 模拟等待异步结果
resp := <-ch
fmt.Printf("模型回复: %s\n", resp)

// 第二次请求 (复用会话)
// 传入上一次的 sid,实现多轮对话
sid2, ch2 := manager.ProcessAsync("user_123", sid, "那你能写代码吗?")

if sid != sid2 {
fmt.Println("Error: Session ID mismatch")
}

resp2 := <-ch2
fmt.Printf("模型回复: %s\n", resp2)

// 验证上下文是否保留
// (实际可通过 manager.GetSession(sid).Messages 查看)
}

第五部分:关键技术点总结与最佳实践

5.1 会话 ID 的安全性

  • 不可预测性: 必须使用加密安全的随机数生成器(如 uuid 库),防止用户遍历 ID 窃取他人对话。
  • 权限绑定: 会话 ID 应与 User IDAPI Key 绑定,查询时需校验归属权。

5.2 上下文截断策略(防止上下文超出模型输入的上限,导致prompt被截断或丢弃等造成token达不到输入预期)

在 Go 实现中,我们使用了简单的条数截断。在生产环境中,建议引入 Token 计数器(如 tiktoken-go,是一个基于Go语言实现的高效BPE(Byte Pair Encoding)分词工具,专门为OpenAI的模型设计。这个项目源自于原生的tiktoken,并为Go开发者提供了方便的接口和性能出色的分词服务):

  1. 计算当前上下文总 Token 数。
  2. 若超过模型限制(如 4096),从最早的消息开始移除,直到满足限制。
  3. 保留 System Prompt 永远不被移除。

5.3 分布式环境下的会话管理

上述 Go 代码使用内存存储(map),适用于单节点。若部署在 Kubernetes 等多节点环境:

  • 存储层: 必须将 Session 数据存入 Redis
  • 锁机制: 使用 Redis Distributed Lock (Redlock) 替代 sync.Mutex,确保不同 Pod 间对同一 Session ID 的互斥访问。
  • ID 生成: 确保 UUID 生成算法在所有节点一致。

5.4 异步状态查询接口

除了推送(SSE),还应提供轮询接口:
GET /api/session/{session_id}/status
返回:{ "status": "processing", "progress": 0.5 }{ "status": "completed", "response": "..." }


结语

会话 ID 是连接无状态模型与有状态人类意图的桥梁。无论是使用 OpenAI 的托管服务,还是自建 vLLM 集群,理解会话管理的底层原理都是开发者必须要掌握的基本常识。

通过一个DIY demo的简单实现展示了如何通过唯一标识生成线程安全的上下文维护以及异步任务关联,构建一个健壮的 LLM 应用后端。在实际生产环境场景中,请根据业务规模,将内存存储升级为 Redis 集群,并引入更精细的 Token 管理策略,以平衡成本与体验

核心公式: 有效对话 = 唯一会话 ID + 持久化上下文 + 并发控制

最后希望所有的开发者,能成功地转入AI应用开发思维,成为一名合格的AI应用开发工程师,其实有传统开发的经验的工程师,更具备快速成为AI应用开发工程师的资格。身边有很多同学朋友都说感觉力不从心,其实大多数的焦虑源于对新事物的不理解,真正理解了就会祛魅,培养和享受AI时代的开发乐趣

有点儿啰嗦,如有误欢迎批评指正交换意见,谢谢。🤝

交流联系方式: https://github.com/ljq
微信 WeChat:labsec
邮箱 Email: ljqlab@163.com

LLM大模型会话ID身份跟踪标识原理解构:从模型无状态下的会话ID(Session ID)原理分析以及自主实现会话跟踪

https://www.wdft.com/20d2dfe9.html

Author

Jaco Liu

Posted on

2026-01-18

Updated on

2026-03-19

Licensed under