先说一个很常见的场景:你在 A 平台上调试了半小时的 Agent 工作流,刚把工具调用链路跑通,却发现团队另一个成员用的是 B 平台的 Agent,或者公司采购的 RAG 服务只支持另一套 API。这时你想把这段对话继续下去,往往只能复制粘贴最后的输出,甚至重新从零开始喂上下文。这种“换一个工具就相当于换一个脑子”的体验,就是 AI Agent 领域越来越被关注的lock-in(供应商锁定)问题。
这篇文章不讨论哲学意义上的“Agent 是否应该有意识”,而是聚焦一个非常工程化的问题:如何让一个 Agent 开始的对话,能无缝交给另一个 Agent 继续。全文会从概念拆解、互操作原理、协议设计、代码实现到踩坑排查,给出一个可直接落地的思路。如果你正在做 Agent 应用开发、RAG 系统集成,或者刚接触 AI Agent 开发并想避开未来的迁移陷阱,这篇文章应该能帮你省下不少时间。
1. 背景与核心概念
1.1 什么是 AI Agent Lock-in
传统软件里的 lock-in,往往体现在数据库选型、云厂商绑定、私有协议这几个方面。到了 AI Agent 时代,lock-in 问题变得更加隐蔽,也更难处理。
一个 AI Agent 的核心能力,不只是“调用大模型接口”,而是围绕一次目标任务维护一套完整的交互状态。这套状态包括:
- 用户的原始意图和多次对话记录。
- Agent 自己规划出来的执行步骤(Plan)。
- 每一步执行时用到的工具名称、入参、返回值。
- 已经收集到的中间结果,比如查到的订单号、生成的报告草稿。
- Agent 内部维护的短期记忆和长期记忆。
如果这套状态只存在于某一个 Agent 产品内部,无法导出、无法标准化、无法被其他 Agent 理解,那么你一旦切换平台,之前的“思考过程”和“执行进度”就会全部丢失。
从这个角度看,AI Agent 的 lock-in 是比 API 接口锁定更严重的锁定。API 锁定只是换一个 SDK、改几行请求,而 Agent 状态锁定意味着你辛辛苦苦构建的上下文资产全部作废。
那为什么过去很少有人解决这件事?原因是 AI Agent 本身还在快速演化,几乎每三个月就会有一批新的 Agent 框架和协议出现。在标准还没成型的时候谈互操作,确实容易被看成“过早设计”。但从工程经验来看,有些基础性的抽象现在就可以做,而且做了之后收益非常明显。
1.2 对话连续性的本质
标题里有一句话很关键:Start conversation with one agent – continue with another。这句话看起来像产品口号,但它背后其实是一个严格的技术命题:
给定两个不同的 Agent 运行时 A 和 B,如果 A 已经与用户完成了一轮或多轮对话,那么 B 能否在完全不丢失上下文的前提下,从 A 的最后一轮输出继续工作?
要实现这个目标,仅仅把 A 的最终回答文本传给 B 是不够的。B 需要知道的不只是“刚才说了什么”,还包括:
- 用户的目标是什么。
- 已经完成了哪些步骤。
- 当前处于哪个阶段。
- 有哪些约束条件和偏好。
- 后续还有哪些待办步骤。
也就是说,连续性的本质是状态迁移,而不是文本复制。一次成功的 Agent 会话,应该像一份工程文档一样,不仅包含结论,还包含可追溯的中间过程。
1.3 为什么需要跨 Agent 会话延续
有人可能会问:我直接在一个平台里把 Agent 用完不就行了?为什么非要跨 Agent 继续对话?
结合我自己的实践经历,跨 Agent 会话延续在下面几类场景中是刚需:
场景一:同任务多工具协作
一个复杂业务往往需要多个专业 Agent 配合。比如一个 Agent 负责对话理解,另一个 Agent 负责代码生成,还有一个 Agent 负责调用内部 API。用户希望只在对话层聊一次,后台由多个 Agent 接力完成。此时如果这些 Agent 来自不同厂商,或者由不同团队开发,它们之间必须能交换上下文。
场景二:平台迁移与升级
公司早期为了快速上线,选择了一款轻量 Agent 产品。等用户量和数据量上来后,发现它在权限控制、审计日志、私有化部署等方面满足不了要求,需要换到另一个平台。如果会话状态不能平滑迁移,老用户的每个会话都会断掉重来,这对体验来说几乎是灾难。
场景三:多端无缝切换
用户在 Web 端与 Agent 交流到一半,切换到手机 App 上希望接着聊。如果 Web 端和 App 端底层接入的是不同 Agent 产品,就需要一个统一的会话中间层。
场景四:容灾与高可用
主 Agent 服务出现故障时,流量切换到备用 Agent。如果两个 Agent 的对话状态不互通,降级体验就会变成“从头再来”。
2. 跨 Agent 会话延续的技术难点
理解了目标之后,再来看看实现这件事到底难在哪里。
2.1 上下文表示不一致
不同 Agent 框架对 Context(上下文)的建模方式差别很大。有的把上下文理解成纯文本数组,每条消息只有 role 和 content;有的则把上下文构建成结构化的消息对象,包含 tool_call_id、tool_calls、name 等字段;还有一些 Agent 框架为了支持复杂工作流,会维护一个 DAG 图,多个节点之间共享不同的上下文片段。
上下文表示不一致,带来的直接后果是:即使你知道需要传递数据,也不知道对方期望的数据格式是什么。就好比中国人和德国人用中文和德语各自写了一本技术手册,互相之间不能直接阅读对方的目录。
2.2 执行状态无法通过 API 获取
很多商业化 Agent 平台只开放了“发起对话”和“获取回答结果”两个接口。对于 Agent 内部的中间步骤——比如它调用了哪几个工具、每个工具返回了什么、它已经执行到哪一步——平台并不提供导出能力。
没有中间步骤信息,就无法构建可恢复的会话状态。你能拿到的只是一个最终回答文本,这就像看了一场魔术表演,只知道结果,但不清楚中间的手法。
2.3 工具调用映射困难
Agent 与普通聊天机器人最大的区别之一是它可以调用工具。在 Agent A 里,一个查询订单的 API 叫queryOrder,参数是orderId。在 Agent B 里,同样的能力可能叫get_order_info,参数是order_no。
会话延续时,如果 A 产生的工具调用记录直接传给 B,B 可能完全无法理解。更麻烦的是,有些工具返回的数据里包含了大量内部数据结构的细节,这些细节在 B 的上下文模型里可能没有对应字段。
2.4 记忆机制无法互通
现代 Agent 产品普遍加入了短期记忆和长期记忆。短期记忆负责把最近的对话压缩成摘要,长期记忆会把关键信息存入向量数据库或知识图谱。
问题在于,每家产品的记忆存储格式和检索策略都是私有的。A 平台生成的记忆摘要,B 平台检索不到;B 平台的知识向量,A 平台也用不了。这导致会话延续时,能传递的往往只有最近几轮的原始文本,而更早的有价值信息被压缩后留在了原平台内部。
2.5 会话安全边界
跨 Agent 会话延续还涉及安全边界问题。Agent A 在会话过程中可能获取了用户的私有数据,这些数据能否传递给 Agent B?如果可以传递,传输过程如何加密?传递过去后,Agent B 的日志系统是否会记录这些数据?这些都是在设计跨 Agent 延续方案时必须提前考虑的合规问题。
3. 核心设计思路:让 Agent 对话变成可携带的会话状态
面对上面这些难点,一个可行的工程思路是:不要把 Agent 切换理解为“文本交接”,而是把会话整体抽象为一种可导出、可导入、可校验的标准化状态对象。
3.1 会话状态数据模型
在设计跨 Agent 会话延续方案时,我通常会定义一个统一的会话状态结构。这个结构不是某个 Agent 平台私有格式的简单复制,而是综合考虑了大部分 Agent 产品都具备的公共属性。
结构上至少需要包含以下部分:
| 状态字段 | 说明 |
|---|---|
| session_id | 会话唯一标识 |
| agent_id | 当前负责该会话的 Agent 标识 |
| user_id | 用户标识,用于权限控制 |
| message_history | 多轮对话消息列表 |
| current_goal | 当前会话的核心目标 |
| plan_steps | 已经规划好的执行步骤,包括已完成和待执行 |
| tool_executions | 已经执行过的工具调用记录和返回结果 |
| memory_context | 短期摘要与关键事实信息 |
| metadata | 自定义扩展字段,兼容各平台专有信息 |
这个结构的关键点在于:它把对话文本和 Agent 执行状态拆开了。对话文本只是状态的一部分,更重要的是执行记录和计划。这样,即使换了一个 Agent 产品,新 Agent 也能快速理解“现在进行到哪一步”。
3.2 标准化上下文交换格式
有了状态模型,还需要定义交换格式。在实践中,我推荐使用 JSON 作为基础序列化格式,因为它的通用性和可读性最好。
一个标准的会话状态导出文件可以这样设计:
{ "schema_version": "1.0", "session_id": "sess_20250115_001", "agent_id": "agent-a", "user_id": "user_123", "current_goal": "查询本月销售数据并生成分析报告", "message_history": [ { "role": "user", "content": "我想看一下本月销售情况", "timestamp": "2025-01-15T10:00:00Z" }, { "role": "assistant", "content": "好的,我来查询本月的销售数据。", "timestamp": "2025-01-15T10:00:05Z" }, { "role": "tool_call", "name": "query_sales", "arguments": { "month": "2025-01" }, "timestamp": "2025-01-15T10:00:06Z" }, { "role": "tool_result", "name": "query_sales", "content": { "total_amount": 1280000, "order_count": 3200 }, "timestamp": "2025-01-15T10:00:08Z" } ], "plan_steps": [ { "step_id": "step_1", "description": "查询本月销售数据", "status": "completed", "result": "总销售额 128 万,订单数 3200" }, { "step_id": "step_2", "description": "按区域拆分销售数据", "status": "pending" }, { "step_id": "step_3", "description": "生成销售分析报告", "status": "pending" } ], "tool_executions": [ { "tool_name": "query_sales", "input": { "month": "2025-01" }, "output": { "total_amount": 1280000, "order_count": 3200 }, "status": "success" } ], "memory_context": { "summary": "用户关注本月销售数据,需要按区域拆解并生成报告", "key_facts": { "period": "2025-01", "currency": "CNY" } }, "metadata": { "source_platform": "agent-a", "exported_at": "2025-01-15T10:05:00Z", "custom_fields": { "agent_a_specific_flag": true } } }这份 JSON 结构解决了一个核心问题:任何 Agent 平台,只要实现一个标准的导入解析器,就能读取另一个平台导出的会话状态。即使某个平台的私有信息无法被对方理解,也会保存在metadata.custom_fields中,不会因为格式不兼容而丢失。
3.3 兼容层与适配器模式
虽然标准化格式很理想,但现实世界里的 Agent 产品不会主动遵循你的格式。所以工程上需要一个适配层,把每个 Agent 平台的私有格式转换成统一格式。
这个适配层的设计模式比较接近企业集成中常用的消息适配器。每个 Agent 平台对应一个适配器类,负责两件事:
- 导出时,从平台私有对象转换为统一会话状态对象。
- 导入时,从统一会话状态对象转换为目标平台能接受的输入格式。
适配层带来的最大收益是:当新增一个 Agent 平台时,只需要开发一个新适配器,不需要修改业务逻辑和上层调用。
3.4 上下文摘要与压缩
有一个现实情况需要接受:并不是所有平台都支持导入完整的工具调用记录。有些平台的 API 只允许你设置一段 system prompt 或者一个初始用户消息。
这种情况下,为了保证更高程度的兼容性,我们需要一个摘要生成器。它把结构化的会话状态压缩成一段自然语言描述,供目标 Agent 理解:
你是本次任务的接续执行者。 当前用户目标是:查询本月销售数据并生成分析报告。 已完成步骤: 1. 已查询本月销售总额,结果为 128 万,订单数 3200。 下一步需要执行: 1. 按区域拆分销售数据。 2. 基于拆分结果生成销售分析报告。 用户偏好:报告需要包含同比和环比对比。这段摘要看起来简单,但在实际跨 Agent 场景中非常实用。它不需要目标 Agent 理解复杂的数据模型,只需要它有足够的阅读能力和执行能力。对于目前主流的大语言模型来说,这种文本引导足以让新 Agent 从断点继续工作。
4. 完整实战:构建一个可延续会话的 Agent 消息中间层
理论部分讲完了,下面进入代码实战。这个项目的目标是构建一个轻量级的Agent 会话消息中间层(Agent Conversation Broker),它不直接与大模型交互,而是负责接收不同 Agent 的会话状态、统一转换格式、存储、并支持在其他 Agent 中恢复会话。
为了便于演示,我会用 Java + Spring Boot 实现一个简化版本。如果你熟悉 Python FastAPI,也可以参考同样的思路重写。
4.1 项目结构设计
先创建项目基础结构:
agent-conversation-broker/ ├── pom.xml ├── src/main/java/com/example/agentbroker/ │ ├── AgentBrokerApplication.java │ ├── controller/ │ │ └── ConversationController.java │ ├── model/ │ │ ├── SessionState.java │ │ ├── MessageRecord.java │ │ └── PlanStep.java │ ├── service/ │ │ └── ConversationTransferService.java │ ├── adapter/ │ │ ├── AgentAdapter.java │ │ ├── AgentAAdapter.java │ │ └── AgentBAdapter.java │ └── storage/ │ └── SessionStateStore.java └── src/main/resources/ └── application.yml这个项目结构本身也体现了一种设计思想:controller负责 API 入口,service负责业务逻辑,adapter负责对接不同的 Agent 平台,storage负责状态存储。每一层的职责都比较单一,后续扩展新 Agent 时不需要修改 controller 和 service 的代码。
4.2 Maven 依赖配置
在pom.xml中加入核心依赖:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.1</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>说明一下为什么引入 Redis:会话状态在跨 Agent 切换时需要被多个节点访问,Redis 在这里作为共享状态存储,保存序列化后的会话状态对象。如果你的项目没有 Redis 环境,也可以先用 ConcurrentHashMap 做内存版实现。
本文示例可以先用内存存储,方便你快速运行验证。Redis 存储可以等确认逻辑正确后再接上。
4.3 定义统一会话状态模型
新建SessionState.java:
package com.example.agentbroker.model; import lombok.Data; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Data public class SessionState { private String schemaVersion = "1.0"; private String sessionId; private String agentId; private String userId; private String currentGoal; private List<MessageRecord> messageHistory = new ArrayList<>(); private List<PlanStep> planSteps = new ArrayList<>(); private Map<String, Object> memoryContext = new HashMap<>(); private Map<String, Object> metadata = new HashMap<>(); }MessageRecord.java用来表示统一的消息记录:
package com.example.agentbroker.model; import lombok.Data; import java.util.Map; @Data public class MessageRecord { private String role; private String content; private String name; private Map<String, Object> arguments; private Map<String, Object> toolResult; private String timestamp; }PlanStep.java用来描述执行步骤:
package com.example.agentbroker.model; import lombok.Data; @Data public class PlanStep { private String stepId; private String description; private String status; private String result; }模型设计里有几个细节值得注意:
- 使用
Map<String, Object>而不是强类型字段来保存工具调用参数和返回值,是为了兼容不同 Agent 平台的异构数据。 schemaVersion字段预留在版本升级时使用,后续如果模型结构变化,可以通过它判断兼容性。- 所有字段尽量使用包装类型和默认值,避免空指针问题。
4.4 适配器接口与实现
定义一个基础适配器接口AgentAdapter.java:
package com.example.agentbroker.adapter; import com.example.agentbroker.model.SessionState; public interface AgentAdapter { /** * 获取当前适配器支持的 Agent 标识 */ String getAgentType(); /** * 从平台私有格式导出为统一 SessionState */ SessionState exportSession(String platformSessionId); /** * 将统一 SessionState 导入目标 Agent */ String importSession(SessionState state); }然后实现两个模拟适配器。
AgentAAdapter.java:
package com.example.agentbroker.adapter; import com.example.agentbroker.model.SessionState; import org.springframework.stereotype.Component; @Component public class AgentAAdapter implements AgentAdapter { @Override public String getAgentType() { return "agent-a"; } @Override public SessionState exportSession(String platformSessionId) { // 模拟:从 Agent A 平台加载会话状态,并转换为统一格式 // 实际项目中,这里需要调用 Agent A 提供的 REST API SessionState state = new SessionState(); state.setSessionId(platformSessionId); state.setAgentId("agent-a"); state.setCurrentGoal("查询本月销售数据并生成分析报告"); return state; } @Override public String importSession(SessionState state) { // 模拟:向 Agent A 平台创建新会话,传入统一状态 // 实际项目中,这里需要调用 Agent A 的创建会话接口 return "agent-a-session-" + state.getSessionId(); } }AgentBAdapter.java的结构类似,只是getAgentType()返回agent-b。可以这样写:
package com.example.agentbroker.adapter; import com.example.agentbroker.model.SessionState; import org.springframework.stereotype.Component; @Component public class AgentBAdapter implements AgentAdapter { @Override public String getAgentType() { return "agent-b"; } @Override public SessionState exportSession(String platformSessionId) { SessionState state = new SessionState(); state.setSessionId(platformSessionId); state.setAgentId("agent-b"); state.setCurrentGoal("查询本月销售数据并生成分析报告"); return state; } @Override public String importSession(SessionState state) { return "agent-b-session-" + state.getSessionId(); } }这两个适配器目前是模拟实现,但接口的方向已经确定。真实对接时,只需要在exportSession中调用平台提供的查询接口,在importSession中调用创建会话接口,并把字段做一个映射即可。
4.5 核心服务:会话转移逻辑
核心服务ConversationTransferService.java是整套机制的中枢:
package com.example.agentbroker.service; import com.example.agentbroker.adapter.AgentAdapter; import com.example.agentbroker.model.SessionState; import com.example.agentbroker.storage.SessionStateStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; import java.util.Map; @Service public class ConversationTransferService { @Autowired private SessionStateStore stateStore; @Autowired private List<AgentAdapter> adapters; private Map<String, AgentAdapter> adapterMap = new HashMap<>(); /** * 初始化时构建 adapter 映射表 */ @jakarta.annotation.PostConstruct public void init() { for (AgentAdapter adapter : adapters) { adapterMap.put(adapter.getAgentType(), adapter); } } /** * 从源 Agent 导出会话,并保存到统一存储 */ public String exportConversation(String sourceAgentType, String platformSessionId) { AgentAdapter sourceAdapter = adapterMap.get(sourceAgentType); if (sourceAdapter == null) { throw new IllegalArgumentException("Unsupported agent type: " + sourceAgentType); } // 调用适配器导出 SessionState state = sourceAdapter.exportSession(platformSessionId); // 保存到统一存储 stateStore.save(state); // 返回统一会话 ID,用于后续导入 return state.getSessionId(); } /** * 将统一会话导入目标 Agent */ public String importConversation(String targetAgentType, String sessionId) { AgentAdapter targetAdapter = adapterMap.get(targetAgentType); if (targetAdapter == null) { throw new IllegalArgumentException("Unsupported agent type: " + targetAgentType); } // 从统一存储加载会话状态 SessionState state = stateStore.load(sessionId); if (state == null) { throw new RuntimeException("Session not found: " + sessionId); } // 修改当前负责的 agent state.setAgentId(targetAgentType); // 调用适配器导入 return targetAdapter.importSession(state); } /** * 追加一条人工消息并更新会话状态 */ public void appendMessage(String sessionId, String role, String content) { SessionState state = stateStore.load(sessionId); if (state == null) { throw new RuntimeException("Session not found: " + sessionId); } Map<String, Object> message = new HashMap<>(); message.put("role", role); message.put("content", content); message.put("timestamp", java.time.Instant.now().toString()); // 这里简化处理,实际项目中需要创建 MessageRecord // state.getMessageHistory().add(message); stateStore.save(state); } }init()方法中用@PostConstruct在服务启动时构建适配器映射,这样后续只需按代码注入即可。
SessionStateStore.java先提供一个内存版存储:
package com.example.agentbroker.storage; import com.example.agentbroker.model.SessionState; import org.springframework.stereotype.Component; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @Component public class SessionStateStore { private Map<String, SessionState> store = new ConcurrentHashMap<>(); public String save(SessionState state) { if (state.getSessionId() == null || state.getSessionId().isEmpty()) { state.setSessionId(UUID.randomUUID().toString()); } store.put(state.getSessionId(), state); return state.getSessionId(); } public SessionState load(String sessionId) { return store.get(sessionId); } public void delete(String sessionId) { store.remove(sessionId); } }接入 Redis 时,可以把save和load方法改为操作 Redis 字符串,序列化工具可以用 Jackson 的 ObjectMapper。这里的内存版足够展示业务逻辑。
4.6 编写控制器接口
有了服务层,还需要对外暴露 API。新建ConversationController.java:
package com.example.agentbroker.controller; import com.example.agentbroker.model.SessionState; import com.example.agentbroker.service.ConversationTransferService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/conversation") public class ConversationController { @Autowired private ConversationTransferService transferService; /** * 导出会话:从源 Agent 获取状态,返回统一会话 ID * 请求示例: * POST /api/conversation/export * { * "sourceAgentType": "agent-a", * "platformSessionId": "a-12345" * } */ @PostMapping("/export") public Map<String, String> exportConversation(@RequestBody Map<String, String> request) { String sessionId = transferService.exportConversation( request.get("sourceAgentType"), request.get("platformSessionId") ); return Map.of("sessionId", sessionId); } /** * 导入会话:把统一会话状态导入到目标 Agent * 请求示例: * POST /api/conversation/import * { * "targetAgentType": "agent-b", * "sessionId": "uuid" * } */ @PostMapping("/import") public Map<String, String> importConversation(@RequestBody Map<String, String> request) { String platformSessionId = transferService.importConversation( request.get("targetAgentType"), request.get("sessionId") ); return Map.of("platformSessionId", platformSessionId); } /** * 查看当前统一会话状态 */ @GetMapping("/{sessionId}") public SessionState getSession(@PathVariable String sessionId) { return transferService.getSession(sessionId); } }为了支持getSession,需要给ConversationTransferService加一个查询方法:
public SessionState getSession(String sessionId) { return stateStore.load(sessionId); }这样一个最小可运行的会话中间层就完成了。它的执行流程是:
- 用户在 Agent A 中开始对话。
- 如果想切换到 Agent B,先调用
/api/conversation/export,把 Agent A 的会话状态导出为统一格式,并保存到统一存储。 - 接着调用
/api/conversation/import,指定目标 Agent B 和统一会话 ID。 - Agent B 通过适配器加载状态,并在自己的平台上创建新会话继续执行。
4.7 运行与验证
启动 Spring Boot 应用后,可以通过 curl 来模拟一次完整流程。
第一步,从 Agent A 导出会话:
curl -X POST http://localhost:8080/api/conversation/export \ -H "Content-Type: application/json" \ -d '{"sourceAgentType": "agent-a", "platformSessionId": "a-12345"}'预期返回:
{"sessionId":"生成的-uuid"}第二步,查询导出的会话状态:
curl http://localhost:8080/api/conversation/生成的-uuid预期返回的是一个 SessionState JSON,包含agentId、currentGoal等字段。
第三步,将会话导入 Agent B:
curl -X POST http://localhost:8080/api/conversation/import \ -H "Content-Type: application/json" \ -d '{"targetAgentType": "agent-b", "sessionId": "生成的-uuid"}'预期返回:
{"platformSessionId":"agent-b-session-生成的-uuid"}到这里,一次完整的“Agent A 开始对话,Agent B 继续对话”就完成了。
5. 进阶方案:不用代码也能延续会话
上面的中间层方案适合有开发资源的团队。如果你暂时不想引入一套中间层系统,只是希望在日常使用或小型项目中减少 Agent lock-in 的影响,也可以采用一种更轻量的思路:把会话状态导出为人类可读的 Markdown / Text 文件,再作为新 Agent 的初始提示词导入。
我这里整理了一份导出模板:
# Agent 会话状态导出 ## 会话信息 - 原平台:Agent A - 原始会话ID:xxx - 导出时间:2025-01-15 10:05:00 ## 用户目标 查询本月销售数据并生成分析报告 ## 已完成步骤 1. [完成] 查询本月销售总额,结果为 128 万,订单数 3200 - 工具:query_sales - 入参:{"month": "2025-01"} - 返回:{"total_amount": 1280000, "order_count": 3200} ## 待执行步骤 1. [待执行] 按区域拆分销售数据 2. [待执行] 基于拆分结果生成分析报告 ## 关键对话记录 用户:我想看一下本月销售情况 助手:好的,我来查询本月的销售数据。 助手:本月销售总额 128 万,订单数 3200,接下来继续按区域拆分。 ## 用户偏好 - 报告需要包含同比和环比对比使用这份 Markdown 文件时,直接把它作为 system prompt 或第一轮用户消息粘贴到新 Agent 中,再追加一句“请从中断的地方继续执行”。对于现代大模型来说,它通常能理解这种结构化文本,并在新会话中继续工作。
这种方法虽然不如标准协议精确,但在没有开发资源的情况下非常实用。它的价值在于让“会话状态”成为用户可以自己掌握的一等公民,而不是被锁定在某个产品内部。
6. 常见问题与排查思路
在实现跨 Agent 会话延续时,有几个高频问题。下面整理成表格,方便排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 从 Agent A 导出的会话状态导入 Agent B 后,B 不认上下文 | 两个平台的消息格式不一致,直接传原始 JSON 无法解析 | 增加适配器做字段映射,必要时转换成目标平台的私有格式 |
| 会话中工具调用记录丢失 | 平台 API 不提供中间步骤查询,只返回最终结果 | 在每次工具调用后,由中间层主动记录工具调用和返回结果,而不是依赖平台提供 |
| 导入新 Agent 后,它重新执行已完成的步骤 | 没有把“已完成步骤”和“待执行步骤”区分清楚 | 在摘要或状态中明确标记每个步骤的完成状态,并告诉新 Agent 不要重复执行已完成步骤 |
| 长期记忆无法恢复 | 各平台的记忆存储格式和检索机制不互通 | 提前设计统一记忆摘要,在导出时把记忆整理成自然语言文本写入 memory_context |
| 切换后涉及用户隐私数据外泄 | 会话状态中包含敏感字段,传输和存储未加密 | 对敏感字段做脱敏或加密,传输使用 HTTPS,统一存储增加访问控制 |
| 新增 Agent 平台时需要改业务代码 | 没有使用适配器模式,业务逻辑与新平台耦合 | 引入适配器模式,新增平台时只实现 AgentAdapter 接口 |
还有一个很容易踩的坑:不要试图把一个平台的完整内部数据结构原样塞给另一个平台。你觉得自己很努力在做兼容,但对方根本不知道你的custom_xxx字段是什么意思。更好的做法是,导出时只保留通用语义,私有字段全部放进 metadata,导入时按需解析。
7. 最佳实践与工程建议
7.1 从第一天就引入会话 ID
如果你想在未来支持跨 Agent 迁移,现在就应该在系统中引入全局唯一的会话 ID。不要依赖各平台的原始会话 ID,而是自己生成 UUID,并在每次调用 Agent 时把会话 ID 透传进去。这样后续做状态同步时,就能建立映射关系。
7.2 状态导出要及时,不要等最后
Agent 执行过程中的中间产物往往比最终结果更有价值。比如代码生成场景中,每一步的修改记录、测试反馈、报错信息,都是恢复会话的关键。建议在每个工具调用完成后,就立即把状态增量同步到统一存储中。不要等到用户手动点击“导出”时才去拉取。
7.3 摘要与结构化数据并存
给新 Agent 传递上下文时,结构化的 JSON 适合程序解析,但自然语言摘要更适合大模型理解。推荐两套都保留:
raw_state:完整的结构化状态,供程序自动化处理。summary_prompt:压缩后的自然语言描述,供新 Agent 快速理解任务背景。
7.4 安全边界与最小权限
跨 Agent 会话延续时,尤其要注意权限边界。新 Agent 应该只获得完成当前任务所需的最小上下文,而不是原始会话的全部敏感信息。建议在导出接口中增加字段过滤配置,只导出允许跨平台共享的字段。
从工程上可以这样实现:
public SessionState exportSession(String platformSessionId, List<String> allowedFields) { // 先导出全部状态 SessionState fullState = agentAdapter.exportSession(platformSessionId); // 按 allowedFields 过滤敏感字段 return filterState(fullState, allowedFields); }7.5 日志与审计
跨 Agent 切换本质上是数据在不同系统之间流动,一定要记录完整的审计日志。至少包括:
- 哪个用户发起了切换。
- 从哪个 Agent 切换到哪个 Agent。
- 导出和导入的会话 ID。
- 切换前后的状态版本。
- 是否有敏感字段被过滤。
有了这些日志,不仅便于排查问题,也能满足合规审计要求。
7.6 不要过度设计
最后想提一点:Agent 互操作还处于早期阶段,不要一上来就搞非常重量级的协议和标准,那是标准组织的工作。对于业务团队来说,先实现一个简单的 JSON 状态导出和导入适配器,已经能解决绝大多数问题。协议的字段设计可以保持精简,后续按实际需要慢慢扩展。
8. 总结与下一步学习方向
回到开头的问题:End AI Agent lock-in,不代表消灭某个平台,也不是要求所有 Agent 产品使用同一套 API。真正的解药是让会话状态成为用户可携带、可理解、可迁移的资产。
本文从概念上拆解了 Agent lock-in 的本质,指出会话连续性不是文本复制而是状态迁移;然后给出了一个标准化的会话状态模型,并结合 Java + Spring Boot 实现了一个支持跨 Agent 会话延续的中间层;最后还提供了一个无需开发也能操作的 Markdown 导出方案,以及一些工程上的最佳实践。
如果你准备把这个方案落到自己的项目里,我建议下一步按这个顺序深入:
- 先梳理自己当前使用的 Agent 平台支持哪些 API,确认能否获取到中间执行步骤。
- 设计一个最小可用的 SessionState 结构,只包含你真实需要的字段。
- 实现一个导出适配器和一个导入适配器,跑通一条路径。
- 把统一存储从内存换到 Redis,解决多实例共享问题。
- 逐步补充安全过滤、审计、摘要生成等能力。
随着 AI Coding Agent、Agent Workflow 编排工具的快速迭代,未来一定会有更多场景需要我们像管理数据库事务一样管理 Agent 上下文状态。在标准成熟之前,先从自己的项目里建立一套可交换的会话状态模型,是一件投资回报非常高的工程决策。