尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零实践Odyssey框架:用Spring Boot 3打造企业AI上下文工程

从零实践Odyssey框架:用Spring Boot 3打造企业AI上下文工程 最近在做一个企业级 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 上下文流转流程一次完整的请求流程可以拆成下面几步用户发起问答携带用户 ID 和问题内容。身份认证模块校验用户身份解析出角色和权限范围。上下文组装器根据问题关键词或意图识别结果确定需要的数据源类型。数据接入层并发查询相关数据比如用户信息、订单记录、规则文档。上下文组装器把数据封装成 BusinessContext并做脱敏和裁剪。PromptBuilder 将 BusinessContext 和用户问题拼装成最终提示词。AiClient 调用大模型得到回答。将本轮回话写入记忆存储用于后续多轮对话。这套流程看着简单但在工程落地时每一步都有很多细节需要考虑。下面我们进入代码部分。3. 环境准备与版本说明3.1 环境清单在动手写代码之前先明确一下本文示例的运行环境。本文示例使用 Java 17 和 Spring Boot 3.xAI 调用部分使用 Spring AI 作为统一抽象层。由于 Spring AI 版本迭代比较快不同版本的 API 有细微差异本文示例以 Maven 坐标和接口思路为主如果遇到 API 变化请以你实际引入的版本为准。环境清单可以参考下面这张表组件推荐版本 / 说明JDK17 或更高Maven3.8Spring Boot3.xSpring 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 version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version relativePath/ /parent groupIdcom.example/groupId artifactIdodyssey-demo/artifactId version1.0.0-SNAPSHOT/version nameodyssey-demo/name descriptionOdyssey Framework Demo/description properties java.version17/java.version spring-ai.version1.0.0/spring-ai.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version${spring-ai.version}/version /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-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 ListContextItem 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 ListContextItem 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 字段用于标识上下文类型比如order、user_profile、policy。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(); ListBusinessContext.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 MapString, String USERS Map.of( 1001, 等级:普通用户, 注册时间:2023-05-01, 历史投诉次数:2, 1002, 等级:VIP用户, 注册时间:2021-11-11, 历史投诉次数:0 ); Override public String type() { return user_profile; } Override public ListBusinessContext.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 MapString, ListString 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 ListBusinessContext.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 ListString POLICIES List.of( 退款规则: 已支付但未发货的订单用户可申请全额退款。, 发货规则: 普通商品付款后48小时内发货预售商品以页面为准。, VIP规则: VIP用户可享受优先发货和专属客服通道。 ); Override public String type() { return policy; } Override public ListBusinessContext.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 ListContextSource contextSources; public ContextAssembler(ListContextSource contextSources) { this.contextSources contextSources; } public BusinessContext assemble(String userId, String role, String query) { BusinessContext context new BusinessContext(userId, role); for (ContextSource source : contextSources) { ListBusinessContext.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 实现 PromptBuilderPromptBuilder 负责把 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?userId1001rolenormalquestion我这单发货太慢了能退款吗预期结果是返回 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 ListBusinessContext.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.enabledfalse odyssey.context.include-policytrue odyssey.context.max-order-count5这样一旦线上效果不达预期可以快速关闭 AI 开关不用立刻回滚版本。框架升级时也要先在一部分流量上灰度观察上下文命中率和用户满意度再全量放开。8. 总结与下一步学习方向这篇文章从一个非常实际的业务问题出发解释了为什么大模型在企业场景里需要业务上下文然后完整介绍了一套代号为 Odyssey 的上下文工程框架。我们实现了从数据源、上下文组装器、PromptBuilder 到 AI 客户端的完整链路并给出了可运行的 Spring Boot 示例。你可以从这几个方向继续深入第一把内存数据源替换成真实的 MySQL、Redis 或接口调用让上下文真正来自你的业务系统。第二把简单的“全量加载”升级为“意图识别 向量检索”让每一轮问答只加载最相关的上下文。第三在框架中接入真实大模型用统一 AiClient 接口切换 OpenAI、DeepSeek、通义千问或本地模型。第四补充 Agent 能力让模型在上下文不足时主动调用工具获取新数据形成更智能的业务问答闭环。建议你从一个小场景开始改造比如先做一个“订单售后问答助手”把用户查询、订单快照和退款规则串起来。等这条路走通之后你会发现企业级 AI 应用的核心难点并不全在模型而在于你是否能稳定、安全、高效地把业务上下文送给模型。多动手实践比读十篇理论文章都有效。
返回列表