从零实践Odyssey框架:用Spring Boot 3打造企业AI上下文工程
2026/9/17 10:07:06 网站建设 项目流程

最近在做一个企业级 AI 问答助手时,我遇到了一个非常典型的问题:大模型本身很聪明,但回答完全不在业务轨道上。比如你问它“帮我判断这个用户能不能办理升级”,模型会先给出通用客服话术,却根本不去查用户当前等级、消费记录和黑名单状态。原因很简单——LLM 没有你公司的业务上下文。它不是不会推理,是缺少推理所依赖的信息。

要让 AI 在真实业务场景里真正落地,不能只调 API,必须额外构建一层“上下文注入层”。本文要讲的 Odyssey Framework,就是围绕这个目标设计的一套上下文工程框架。它不是一个虚无飘渺的概念,而是包含了数据接入、上下文组装、Prompt 构造、记忆管理、权限过滤的完整实现路径。这篇教程会从零开始,用 Spring Boot 3 + Spring AI 演示一个可运行的框架原型,覆盖设计思路、代码实现、常见坑点和工程建议。

1. 背景与核心概念

1.1 为什么 AI 需要业务上下文

先来看看最直观的问题。

现在很多开发团队接入大模型的方式非常简单:把用户输入丢给模型,拿到输出就返回。这在通用对话场景下没问题,但一旦放到企业业务里,就会立刻暴露短板。

举一个很具体的例子。你在电商平台上提问:

用户问题:我这个订单晚了两天还没发货,能退款吗?

如果模型只拿到这句话,它只能给出“请耐心等待”或者“建议联系客服”这类通用回答。它不知道:

  • 这个订单是什么时候下的,属于什么商品类目。
  • 当前是普通订单、预售订单还是代购订单。
  • 平台的售后规则是否允许超时未发货退款。
  • 用户是不是 VIP,有没有历史纠纷记录。

这些信息统称为“业务上下文”。没有它们,模型就相当于一个能力很强但完全失忆的新员工。你问它业务规则,它只能凭训练数据里的通用知识去猜测。

所以,“给 AI 提供业务上下文”并不是一个可选项,而是企业级 AI 应用落地的前置条件。这也是 Odyssey Framework 最核心的出发点。

1.2 Odyssey Framework 是什么

Odyssey Framework 本质上是一套面向 AI 应用的“上下文工程框架”。

它解决的问题可以概括为一句话:让模型在回答每一个问题之前,能够获取到与问题相关的、实时且经过授权的业务数据,并把它们组织成模型能够理解的结构化 Prompt。

从命名上看,Odyssey 有“漫长旅程”的含义。在企业 AI 落地过程中,从模型能力到业务价值本来就是一个探索过程,框架要做的就是帮这条探索路径搭好基础设施。

这里需要和几个容易混淆的概念做区分:

概念侧重点与 Odyssey 的关系
RAG(检索增强生成)从外部知识库检索相关片段Odyssey 的上下文检索模块可以基于 RAG 实现
Prompt Engineering设计提示词模板Odyssey 负责用业务数据动态填充提示词模板
Agent / Function Calling让模型自主决策调用工具Odyssey 提供工具背后的业务数据上下文
传统 ORM / DAO数据持久化访问Odyssey 在数据之上增加“面向模型”的组装逻辑

也就是说,Odyssey 并不是要替代 RAG 或者 Prompt 工程,而是把数据访问、上下文组装和模型调用串成一个完整的链路,让开发者不用在每次问答里手动拼接又长又乱的业务信息。

1.3 适用场景与落地边界

Odyssey 这种框架适合解决的场景包括:

  • 企业内部知识库问答,让模型基于公司制度、产品文档回答。
  • 业务系统中的智能助手,比如订单售后、用户运营、合规审查。
  • 数据分析场景,让模型在特定业务口径下解释数据。
  • Agent 场景,让智能体在决策时能拿到实时业务上下文。

但也要说清楚边界。Odyssey 不是用来替代大模型训练的,它不会让模型凭空学会新知识,它只是把知识包装成模型能理解的输入。如果你需要的是一次性把全网公开知识塞进模型,那应该考虑微调或继续预训练;如果你的问题高度依赖企业私有实时数据,那正是在 Odyssey 的能力范围内。

2. 框架总体设计与核心模块

2.1 分层架构

为了避免代码一团乱,我们在设计 Odyssey Framework 的时候先定了清晰的分层架构。整体来看,整个链路从上到下分为四层:

用户请求 ↓ 控制器层(Controller / API) ↓ 上下文组装层(Context Assembler) ↓ 数据接入层(Data Adapter) ↓ AI 调用层(AI Client) ↓ 大模型

每一层只负责一件事。控制器层接收用户问题和用户身份;上下文组装层负责判断“这个问题需要哪些上下文”;数据接入层负责从数据库、缓存、接口、向量库等来源拉取数据;AI 调用层负责把组装好的上下文和用户问题拼成 Prompt,并调用大模型。

2.2 核心模块职责

在具体实现中,框架可以拆成下面几个模块:

  • BusinessContext 业务上下文对象:统一存放用户信息、业务实体快照、规则片段和对话记忆的模型类。
  • ContextSource 数据源接口:屏蔽底层数据来源差异,可以是 MySQL、Redis、REST API,也可以是向量数据库。
  • ContextAssembler 上下文组装器:根据用户问题语义和意图判断需要加载哪些数据源,并把结果汇总成一个 BusinessContext。
  • PromptBuilder 提示词构造器:把 BusinessContext 转换成结构化的 Prompt 文本。
  • AiClient 模型调用客户端:统一封装大模型 API 调用,推荐用接口隔离,方便切换 OpenAI、通义千问、DeepSeek 或本地模型。
  • PermissionFilter 权限过滤器:在上下文组装前拦截请求,确保用户只能看到权限范围内的数据。

2.3 上下文流转流程

一次完整的请求流程可以拆成下面几步:

  1. 用户发起问答,携带用户 ID 和问题内容。
  2. 身份认证模块校验用户身份,解析出角色和权限范围。
  3. 上下文组装器根据问题关键词或意图识别结果,确定需要的数据源类型。
  4. 数据接入层并发查询相关数据,比如用户信息、订单记录、规则文档。
  5. 上下文组装器把数据封装成 BusinessContext,并做脱敏和裁剪。
  6. PromptBuilder 将 BusinessContext 和用户问题拼装成最终提示词。
  7. AiClient 调用大模型,得到回答。
  8. 将本轮回话写入记忆存储,用于后续多轮对话。

这套流程看着简单,但在工程落地时,每一步都有很多细节需要考虑。下面我们进入代码部分。

3. 环境准备与版本说明

3.1 环境清单

在动手写代码之前,先明确一下本文示例的运行环境。

本文示例使用 Java 17 和 Spring Boot 3.x,AI 调用部分使用 Spring AI 作为统一抽象层。由于 Spring AI 版本迭代比较快,不同版本的 API 有细微差异,本文示例以 Maven 坐标和接口思路为主,如果遇到 API 变化,请以你实际引入的版本为准。

环境清单可以参考下面这张表:

组件推荐版本 / 说明
JDK17 或更高
Maven3.8+
Spring Boot3.x
Spring AI1.0.x 或当前稳定版
IDEIntelliJ IDEA / Eclipse
数据库本文用内存 Map 演示,可扩展为 MySQL
大模型OpenAI / DeepSeek / 通义千问,可替换

版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路,而不是绑定某一个具体版本。如果你本地已经装了不同版本,只要保持核心依赖兼容即可。

3.2 项目结构

我们创建一个名为 odyssey-demo 的 Spring Boot 项目,整体目录结构如下:

odyssey-demo/ ├── pom.xml └── src/main/java/com/example/odyssey/ ├── OdysseyApplication.java ├── context/ │ ├── BusinessContext.java │ ├── ContextAssembler.java │ ├── ContextItem.java │ └── ContextSource.java ├── datasource/ │ ├── OrderRepository.java │ ├── UserRepository.java │ └── PolicyRepository.java ├── ai/ │ ├── AiClient.java │ ├── MockAiClient.java │ └── PromptBuilder.java └── controller/ └── ChatController.java

后面每个文件怎么实现,我会一步步展开。先不急着写代码,先想清楚每个类的职责:

  • BusinessContext是上下文对象,最后要交给 PromptBuilder 使用。
  • ContextSource是数据源统一接口,OrderRepository 和 UserRepository 都会实现它。
  • ContextAssembler负责调度所有数据源并组装上下文。
  • AiClient是模型调用的门面。
  • ChatController对外暴露 HTTP 接口。

3.3 Maven 依赖

pom.xml 中先引入基础依赖。这里只列 Spring Boot Web 和 Spring AI 相关的坐标,并声明依赖版本由 Spring Boot 父级管理。

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>odyssey-demo</artifactId> <version>1.0.0-SNAPSHOT</version> <name>odyssey-demo</name> <description>Odyssey Framework Demo</description> <properties> <java.version>17</java.version> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>${spring-ai.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

需要说明的是,spring-ai-openai-spring-boot-starter会要求配置 OpenAI Key。为了避免没有 Key 的读者跑不起来,后面我会提供一个可切换的MockAiClient,并让它成为默认实现。这样整个示例可以零成本运行。

4. 核心实现

4.1 定义业务上下文模型

BusinessContext 是整个框架的数据核心。它至少要包含三类信息:用户基本信息、业务实体快照、规则或知识片段。

// 文件路径:src/main/java/com/example/odyssey/context/BusinessContext.java package com.example.odyssey.context; import java.util.ArrayList; import java.util.List; public class BusinessContext { private String userId; private String role; private List<ContextItem> items = new ArrayList<>(); public BusinessContext(String userId, String role) { this.userId = userId; this.role = role; } public void addItem(String type, String content) { this.items.add(new ContextItem(type, content)); } public String getUserId() { return userId; } public String getRole() { return role; } public List<ContextItem> getItems() { return items; } public static class ContextItem { private final String type; private final String content; public ContextItem(String type, String content) { this.type = type; this.content = content; } public String getType() { return type; } public String getContent() { return content; } } }

这里要注意:ContextItem 的 type 字段用于标识上下文类型,比如orderuser_profilepolicy。PromptBuilder 在拼接提示词时,可以根据 type 决定如何格式化,这样模型能更清楚地理解每一段信息的来源和用途。

4.2 定义数据源接口

不同业务数据可能来自数据库、缓存、第三方接口。为了统一组装器的调用逻辑,我们定义一个上下文数据源接口:

// 文件路径:src/main/java/com/example/odyssey/context/ContextSource.java package com.example.odyssey.context; import java.util.List; public interface ContextSource { String type(); List<BusinessContext.ContextItem> load(String userId, String query); }

type() 返回该数据源的类型标识,load() 方法根据 userId 和用户问题加载相关上下文片段。返回的 ContextItem 会被组装进 BusinessContext。

这里的设计思路是:数据源只需要关心“当前用户问了什么,需要返回什么数据”,而不需要关心最终 Prompt 长什么样。这能避免数据逻辑和提示词逻辑耦合在一起。

4.3 编写数据源实现

为了演示方便,我用内存数据结构模拟订单、用户和规则。实际项目中,你可以把 Repository 替换为 MyBatis、JPA 或远程接口调用。

先看用户数据源:

// 文件路径:src/main/java/com/example/odyssey/datasource/UserRepository.java package com.example.odyssey.datasource; import com.example.odyssey.context.BusinessContext; import com.example.odyssey.context.ContextSource; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; @Component public class UserRepository implements ContextSource { private static final Map<String, String> USERS = Map.of( "1001", "等级:普通用户, 注册时间:2023-05-01, 历史投诉次数:2", "1002", "等级:VIP用户, 注册时间:2021-11-11, 历史投诉次数:0" ); @Override public String type() { return "user_profile"; } @Override public List<BusinessContext.ContextItem> load(String userId, String query) { String profile = USERS.getOrDefault(userId, "未知用户"); return List.of(new BusinessContext.ContextItem(type(), profile)); } }

再看订单数据源:

// 文件路径:src/main/java/com/example/odyssey/datasource/OrderRepository.java package com.example.odyssey.datasource; import com.example.odyssey.context.BusinessContext; import com.example.odyssey.context.ContextSource; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; @Component public class OrderRepository implements ContextSource { private static final Map<String, List<String>> ORDERS = Map.of( "1001", List.of( "订单 A10001, 下单时间:2025-01-10, 状态:已支付未发货, 商品:数码相机", "订单 A10002, 下单时间:2025-01-05, 状态:已签收, 商品:蓝牙耳机" ), "1002", List.of( "订单 B20001, 下单时间:2025-01-12, 状态:已发货, 商品:运动手表" ) ); @Override public String type() { return "order"; } @Override public List<BusinessContext.ContextItem> load(String userId, String query) { return ORDERS.getOrDefault(userId, List.of()) .stream() .map(order -> new BusinessContext.ContextItem(type(), order)) .toList(); } }

最后写一个规则数据源:

// 文件路径:src/main/java/com/example/odyssey/datasource/PolicyRepository.java package com.example.odyssey.datasource; import com.example.odyssey.context.BusinessContext; import com.example.odyssey.context.ContextSource; import org.springframework.stereotype.Component; import java.util.List; @Component public class PolicyRepository implements ContextSource { private static final List<String> POLICIES = List.of( "退款规则: 已支付但未发货的订单,用户可申请全额退款。", "发货规则: 普通商品付款后48小时内发货,预售商品以页面为准。", "VIP规则: VIP用户可享受优先发货和专属客服通道。" ); @Override public String type() { return "policy"; } @Override public List<BusinessContext.ContextItem> load(String userId, String query) { return POLICIES.stream() .map(policy -> new BusinessContext.ContextItem(type(), policy)) .toList(); } }

4.4 实现上下文组装器

组装器是框架的核心调度器。它会把所有数据源加载的结果收集起来,组装成 BusinessContext。

// 文件路径:src/main/java/com/example/odyssey/context/ContextAssembler.java package com.example.odyssey.context; import org.springframework.stereotype.Component; import java.util.List; @Component public class ContextAssembler { private final List<ContextSource> contextSources; public ContextAssembler(List<ContextSource> contextSources) { this.contextSources = contextSources; } public BusinessContext assemble(String userId, String role, String query) { BusinessContext context = new BusinessContext(userId, role); for (ContextSource source : contextSources) { List<BusinessContext.ContextItem> items = source.load(userId, query); for (BusinessContext.ContextItem item : items) { context.addItem(item.getType(), item.getContent()); } } return context; } }

这里 Spring 会自动把容器中所有实现了 ContextSource 接口的 Bean 注入到 List 中。当你新增一个数据源时,不需要改动组装器,只需要新增一个 Component 即可。这就是面向接口设计带来的扩展性。

4.5 实现 PromptBuilder

PromptBuilder 负责把 BusinessContext 转成模型的输入。它的目标是让模型在回答前先看到上下文,再看到用户问题。

// 文件路径:src/main/java/com/example/odyssey/ai/PromptBuilder.java package com.example.odyssey.ai; import com.example.odyssey.context.BusinessContext; import org.springframework.stereotype.Component; @Component public class PromptBuilder { public String build(BusinessContext context, String userQuery) { StringBuilder prompt = new StringBuilder(); prompt.append("你是一名专业的业务客服助手。请严格基于以下业务上下文回答用户问题。"); prompt.append("\n如果上下文不足以回答问题,请明确告知用户需要补充哪些信息。\n\n"); prompt.append("当前用户ID: ").append(context.getUserId()).append("\n"); prompt.append("当前用户角色: ").append(context.getRole()).append("\n\n"); prompt.append("===== 业务上下文开始 =====\n"); for (BusinessContext.ContextItem item : context.getItems()) { prompt.append("[").append(item.getType()).append("] ") .append(item.getContent()) .append("\n"); } prompt.append("===== 业务上下文结束 =====\n\n"); prompt.append("用户问题: ").append(userQuery).append("\n"); prompt.append("请给出准确、简洁、友善的答复。"); return prompt.toString(); } }

这个 Builder 看起来简单,但在实际工程中会非常关键。上下文越多,Token 开销越大,模型也越容易“迷失”。所以 PromptBuilder 需要做的另一件事就是裁剪和去重,只保留与当前问题最相关的片段。你可以基于关键词匹配、向量相似度或者规则来过滤。

4.6 实现 AI 客户端

为了支持不同模型和方便测试,我们先定义统一的 AiClient 接口:

// 文件路径:src/main/java/com/example/odyssey/ai/AiClient.java package com.example.odyssey.ai; public interface AiClient { String chat(String systemPrompt); }

然后实现一个 MockAiClient。它不调用真实大模型,而是把 Prompt 原样返回,并附加一条模拟结果。这样在本地没有 Key 的情况下也能看到完整调用链。

// 文件路径:src/main/java/com/example/odyssey/ai/MockAiClient.java package com.example.odyssey.ai; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.stereotype.Component; @Component @ConditionalOnMissingBean(AiClient.class) public class MockAiClient implements AiClient { @Override public String chat(String systemPrompt) { return "【MOCK 模型回复】\n" + "我注意到你咨询了订单相关业务。基于当前业务上下文," + "系统判断需要结合用户等级、订单状态和退款规则共同分析。\n\n" + "实际项目中,这里会接入真实大模型返回结构化答复。"; } }

这里用@ConditionalOnMissingBean注解,是希望当项目里出现了其他更具体的 AiClient Bean 时,Mock 实现自动失效。读者拿到代码后,可以直接新增一个 OpenAiClient 来替换。

接下来写一个基于 Spring AI 的真实客户端实现示例。由于不同版本的 Spring AI API 有差异,下面代码给出接口思路,需要按你引入的版本调整:

// 文件路径:src/main/java/com/example/odyssey/ai/SpringAiClient.java package com.example.odyssey.ai; import org.springframework.ai.chat.model.ChatModel; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; @Component @ConditionalOnProperty(name = "odyssey.ai.enabled", havingValue = "true") public class SpringAiClient implements AiClient { private final ChatModel chatModel; public SpringAiClient(ChatModel chatModel) { this.chatModel = chatModel; } @Override public String chat(String systemPrompt) { return chatModel.call(systemPrompt); } }

注意,ChatModel是 Spring AI 1.x 中的常见接口,如果你用的是其他版本,类名可能不同。真实项目中,你可以在 application.yml 中配置模型供应商的 Key。

4.7 实现控制器

最后写 Controller,对外暴露一个最简单的问答接口。

// 文件路径:src/main/java/com/example/odyssey/controller/ChatController.java package com.example.odyssey.controller; import com.example.odyssey.ai.AiClient; import com.example.odyssey.ai.PromptBuilder; import com.example.odyssey.context.BusinessContext; import com.example.odyssey.context.ContextAssembler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class ChatController { private final ContextAssembler contextAssembler; private final PromptBuilder promptBuilder; private final AiClient aiClient; public ChatController(ContextAssembler contextAssembler, PromptBuilder promptBuilder, AiClient aiClient) { this.contextAssembler = contextAssembler; this.promptBuilder = promptBuilder; this.aiClient = aiClient; } @GetMapping("/chat") public String chat(@RequestParam String userId, @RequestParam String role, @RequestParam String question) { BusinessContext context = contextAssembler.assemble(userId, role, question); String prompt = promptBuilder.build(context, question); return aiClient.chat(prompt); } }

到这一步,框架原型已经能跑了。

5. 完整实战案例与运行验证

5.1 场景定义

我们用一个具体的售后场景来测试整个链路。

用户1001是普通用户,有一个“已支付未发货”的订单,他提问:

我这单发货太慢了,能退款吗?

按正常客服逻辑,模型应该结合订单状态和退款规则回答用户:可以申请全额退款。而如果没有业务上下文,模型很可能只能给出模糊的安抚话术。

5.2 创建启动类

com.example.odyssey包下创建启动类:

// 文件路径:src/main/java/com/example/odyssey/OdysseyApplication.java package com.example.odyssey; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class OdysseyApplication { public static void main(String[] args) { SpringApplication.run(OdysseyApplication.class, args); } }

5.3 启动并验证接口

使用 Maven 命令启动项目:

mvn spring-boot:run

启动成功后,在浏览器或命令行工具中访问:

curl "http://localhost:8080/chat?userId=1001&role=normal&question=我这单发货太慢了,能退款吗?"

预期结果是返回 MockAiClient 的输出。虽然这不是真实模型回答,但你已经能从返回内容中看到完整的上下文注入链路。如果你配置了真实模型,SpringAiClient会返回基于业务上下文生成的回答。

5.4 如何确认上下文真的生效了

你可能希望看到最终拼出来的 Prompt 长什么样。这里可以在 Controller 中临时打印,或者直接写一个 Debug 端点,方便验证。

// 文件路径:src/main/java/com/example/odyssey/controller/DebugController.java package com.example.odyssey.controller; import com.example.odyssey.ai.PromptBuilder; import com.example.odyssey.context.BusinessContext; import com.example.odyssey.context.ContextAssembler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class DebugController { private final ContextAssembler contextAssembler; private final PromptBuilder promptBuilder; public DebugController(ContextAssembler contextAssembler, PromptBuilder promptBuilder) { this.contextAssembler = contextAssembler; this.promptBuilder = promptBuilder; } @GetMapping("/debug-prompt") public String debugPrompt(@RequestParam String userId, @RequestParam String role, @RequestParam String question) { BusinessContext context = contextAssembler.assemble(userId, role, question); return promptBuilder.build(context, question); } }

访问/debug-prompt后,你可以直观看到业务上下文是如何被注入 Prompt 的。这个 Debug 接口在初学阶段非常实用,它能把模型调用前的数据链路可视化,帮我们快速判断是上下文的问题还是模型的问题。

5.5 扩展到真实数据源

当前代码使用的是内存 Map 模拟数据源。要扩展到 MySQL,只需要在 OrderRepository 中注入 JdbcTemplate 或 Mapper 接口,把 load 方法改成数据库查询即可。

// 下面的代码是扩展思路,不是完整实现 @Component public class OrderRepository implements ContextSource { private final JdbcTemplate jdbcTemplate; public OrderRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public List<BusinessContext.ContextItem> load(String userId, String query) { String sql = "select order_no, create_time, status, product_name from orders where user_id = ?"; return jdbcTemplate.query(sql, (rs, rowNum) -> new BusinessContext.ContextItem( type(), "订单 " + rs.getString("order_no") + ", 下单时间:" + rs.getString("create_time") + ", 状态:" + rs.getString("status") + ", 商品:" + rs.getString("product_name") ), userId); } }

这样,你就完成了从内存演示到真实数据源的无缝切换。

6. 常见问题与排查思路

6.1 上下文没生效,模型仍然只说通用话术

问题现象常见原因解决思路
模型回答与业务规则无关上下文没有正确注入 Prompt先调用 Debug 接口确认 Prompt 内容
上下文太长,模型忽略关键信息上下文顺序不合理或信息冗余优先把与问题最相关的上下文放在末尾附近
数据源加载失败数据源接口异常被吞掉检查日志,在数据源加载时增加异常捕获

一个很常见的坑是:数据源查询报错,但被上层 catch 掉了,导致业务上下文为空,模型自然只能泛泛回答。建议在 ContextAssembler 里记录数据源加载成功率,并输出日志。

6.2 模型回答出现幻觉

问题现象常见原因解决思路
模型编造不存在的规则上下文中没有明确规则,模型自行推断在 PromptBuilder 中强调“只能基于上下文回答”
模型把示例当成了真实数据上下文示例和真实数据混在一起在 Prompt 中增加数据来源标识
模型拒绝回答系统提示词约束过强平衡约束和自由度,增加“无法确定时请说明”

这里建议大家把“严格基于上下文”写进系统提示词,并且在上下文末尾加一句“如果上下文信息不足,请直接说明,不要猜测”。这会显著减少幻觉。

6.3 上下文越来越多,Token 超限

问题现象常见原因解决思路
上报错 context length exceeded一次性加载了过多历史数据和规则引入上下文裁剪、摘要和向量检索
响应变慢上下文过多导致首字延迟增加精简上下文,模型输入输出都要控制
费用暴涨每次请求都重复注入完整上下文增加缓存,对高频用户做上下文复用

上下文不是越多越好。企业应该为“上下文质量”建立指标,而不是只看“上下文数量”。当你发现模型因为上下文过长而性能下降时,优先做精简和相关性排序。

6.4 多用户数据串扰

问题现象常见原因解决思路
用户 A 看到用户 B 的订单数据源查询没有按 userId 过滤所有数据源 load 方法必须严格按 userId 过滤
权限遗漏Controller 层没有身份校验在网关或拦截器统一做身份解析和授权

多租户和权限隔离是上下文框架最容易出错的地方。安全底线是:每个数据源在查询之前,都要把用户 ID 作为强制过滤条件,而不是依赖上层传入的上下文“碰巧正确”。

7. 最佳实践与工程建议

7.1 把数据权限放在第一位

上下文框架注入的是业务敏感数据,一旦权限没做好,就是数据泄露事故。建议在设计阶段就明确:

所有 ContextSource 的 load 方法都必须接收 userId,并且内部必须基于 userId 做数据过滤。不要允许数据源无参数地返回全量数据。数据源内部还要考虑角色权限,销售角色和普通用户角色能看到的数据范围完全不同。

另外,Redis 等缓存中如果存了上下文内容,一定要按用户维度隔离,并设置合理的过期时间。

7.2 建立上下文质量评估机制

很多团队上线 AI 功能后,只关注“回复好不好看”,忽略了上下文质量。建议引入几个指标:

  • 上下文命中率:加载的上下文中有多少被模型真正用到了。
  • 上下文准确率:加载的数据是否是最新、是否准确。
  • 上下文时效性:业务数据多久同步一次,是否满足实时性要求。
  • 回答有效率:用户是否对回答满意,是否转人工。

通过这些指标,你可以持续优化数据源和组装逻辑。比如发现订单状态经常过期,就需要增加数据源实时查询能力;发现规则片段加载过多,就要加强相关性排序。

7.3 控制 Prompt 长度和 Token 成本

刚才提到过,上下文越多越好是误区。工程上可以采取这些手段:

第一,对历史对话做摘要,不要把所有历史记录都丢给模型。如果用户已经聊了十轮,可以只保留最近两轮完整对话和前面八轮的摘要。

第二,对业务上下文做裁剪。例如用户查询订单退款时,不需要加载他的全部十年订单,只加载近三个月的活跃订单即可。

第三,引入向量检索。把企业知识库切块并向量化,在组装上下文时用相似度检索找出与当前问题最相关的 3 到 5 个片段,而不是把所有规则全部塞进 Prompt。

7.4 做好日志、监控和可观测性

AI 应用的可观测性比传统应用更复杂,因为你不知道模型为什么输出这段话。建议至少记录以下信息:

  • 每次请求的 userId、问题、上下文类型和大小。
  • 最终发送给模型的 Prompt 全文。
  • 模型返回结果和耗时。
  • 上下文组装耗时、数据源明细耗时。

有条件的团队可以使用 LangSmith、Langfuse 或自研的日志链路。没有条件时,至少在 ContextAssembler 里记录每个数据源的加载耗时和结果数量。排错时,这些日志会救命。

7.5 灰度发布与线上回滚

如果上下文框架要接入生产环境,建议用开关控制。比如通过配置中心或环境变量控制哪些用户走 AI 问答,哪些走传统规则引擎。

# application.yml 或配置中心 odyssey.ai.enabled=false odyssey.context.include-policy=true odyssey.context.max-order-count=5

这样一旦线上效果不达预期,可以快速关闭 AI 开关,不用立刻回滚版本。框架升级时也要先在一部分流量上灰度,观察上下文命中率和用户满意度再全量放开。

8. 总结与下一步学习方向

这篇文章从一个非常实际的业务问题出发,解释了为什么大模型在企业场景里需要业务上下文,然后完整介绍了一套代号为 Odyssey 的上下文工程框架。我们实现了从数据源、上下文组装器、PromptBuilder 到 AI 客户端的完整链路,并给出了可运行的 Spring Boot 示例。

你可以从这几个方向继续深入:

第一,把内存数据源替换成真实的 MySQL、Redis 或接口调用,让上下文真正来自你的业务系统。

第二,把简单的“全量加载”升级为“意图识别 + 向量检索”,让每一轮问答只加载最相关的上下文。

第三,在框架中接入真实大模型,用统一 AiClient 接口切换 OpenAI、DeepSeek、通义千问或本地模型。

第四,补充 Agent 能力,让模型在上下文不足时主动调用工具获取新数据,形成更智能的业务问答闭环。

建议你从一个小场景开始改造,比如先做一个“订单售后问答助手”,把用户查询、订单快照和退款规则串起来。等这条路走通之后,你会发现企业级 AI 应用的核心难点并不全在模型,而在于你是否能稳定、安全、高效地把业务上下文送给模型。多动手实践,比读十篇理论文章都有效。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询