企业中的春季人工智能:配备工具、RAG及审核功能的客服代理
用于事件分诊助手的Spring AI:以提示词作为契约、结构化输出、工具调用、MCP发现、Qdrant RAG、追踪功能以及受限的智能体循环。
来源: Leveraging Spring AI for building the next generation of enterprise applications
以支持工单作为测试用例
某金融科技公司为软件工程师设立的内部AI工程工作坊,正是源于几乎每位开发者迟早都会遇到的场景:有一份支持工单送达,日志中只有部分证据,相关代码则存放在内部代码库中。负责处理工单的人需要的是针对性的初步分析,而非泛泛的答复。
Spring AI非常适合这类场景,因为它将AI功能整合到了Java和Spring开发者日常已使用的生态系统中。它提供了针对模型、提示词、结构化输出、工具调用与检索的抽象层,这些功能能够几乎毫无障碍地融入现有的应用程序架构中。
从 Java 应用程序调用大型语言模型并非最有趣的部分。真正重要的是要在遵循金融科技领域所要求的规范的前提下完成这一操作:明确的契约、最小权限的工具、可追溯性,以及在任何涉及重要决策时都需进行人工审核。
因此,这里的讨论范围超出了研讨会的内容。一个“客服代理”项目被用作实例,展示如何将人工智能与现有系统整合,模型在架构中的定位,它如何为企业提供能力和数据支持,以及周边系统如何确保其行为始终有用、可预测且处于可控状态。
这是集成问题,而非模型问题
将人工智能集成到企业系统中主要是整合工作,与选择模型关系不大。团队通常在第一个原型之后才会意识到这一点。向模型发送提示语很简单,得到的结果也可能看起来相当出色。然而,一旦人工智能被引入生产系统,一些基本矛盾就会显现出来。
企业软件是以确定性为基础构建的。它依赖于严格的规则、可预测的延迟以及明确的数据流。相同的输入应当产生一致的输出,其行为也可以通过代码进行追踪。
人工智能系统的运作方式则不同。它们本质上是概率性的,常常以自然语言代替结构化输入,并且依赖外部服务提供商。同样的请求可能会得到不同的答案,而且部分逻辑存在于模型内部,超出了系统的控制范围。
其结果便是结构上的不匹配:一个非确定性组件被嵌入到确定性架构之中。关于测试、监控和可靠性的传统假设不再适用。难点不在于生成巧妙的响应,而在于将这些响应融入那些强调控制的系统中。
Spring AI的处理方式是将人工智能视为Spring生态系统中又一个需要整合的组件,而非特例。Spring早已对数据库、消息传递和REST调用进行了抽象,而Spring AI则为语言模型、向量存储以及人工智能驱动的工作流提供了类似的抽象层。其目的并非完全隐藏复杂性,而是对复杂性进行组织,使其符合现有的架构模式。
在典型的架构中,人工智能并不独立存在于某个单独的服务中,而是作为集成代码嵌入在应用层之中。
该项目的处理逻辑如下:当有请求到来时,常规业务请求会被拒绝,而真正的故障则会生成工单。Spring AI工具会读取日志并进行分析,然后将诊断结果转换为Java记录。随后从基于Qdrant的向量存储中获取相关源代码,生成修复建议,并更新工单以便相关人员查看。
对开发者而言,AI只是另一个依赖组件,应用程序依然掌控着整个工作流程。模型可以像其他基础设施一样进行配置,所有交互都通过定义良好的API完成,而非临时的HTTP调用。
在项目中,SupportAgentController以SupportTicketRequest的形式接收传入请求。它将人工智能的推理结果传递给SupportAnalysisService,通过SupportTicketService保存状态,利用GithubRagService获取源代码,并将生成的解决方案存储在工单中。
一个显著的副作用是业务逻辑与模型提供方相互独立。即便更换提供方或添加备用方案,核心代码也不会受到影响。从长远来看,这更多是为了保持架构控制权而非灵活性。如果无法访问模型,下面的接口仍应创建工单,将人工智能分析标记为不可用,并将案例转交至人工处理队列。
@PostMapping("/requests")
public ResponseEntity<?> submitSupportRequest(@Valid @RequestBody SupportTicketRequest request) {
boolean isBusiness = supportAnalysisService.isItBusinessRequest(request);
if (isBusiness) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponse.businessRequest());
}
SupportTicket ticket = ticketService.createTicket(request);
LogCauseReport logCauseReport = supportAnalysisService.analyzeLogs(request);
String classCode = ragService.findRelevantClass(null, logCauseReport.repoName(), null, logCauseReport.className()).stream()
.map(SupportAgentController::stripMetadataLines)
.filter(content -> !content.isBlank())
.collect(Collectors.joining(System.lineSeparator() + System.lineSeparator()));
String solution = supportAnalysisService.suggestFix(classCode, logCauseReport.causeOfProblem());
ticketService.updateTicket(ticket.getId(), SupportStatus.SUBMITTED,
"Cause of the problem: " + logCauseReport.causeOfProblem() + "\n\nSuggested fix: " + solution);
return ResponseEntity.status(HttpStatus.CREATED).body(SupportTicketResponse.from(ticket));
}
ChatClient与作为契约的提示词
Spring AI以ChatClient API为核心,这是一种与语言模型进行交互的结构化方式。开发者无需手动构建HTTP请求,而是可以使用这套功能完善的API来处理提示词的构造、执行以及响应处理。
在Support Agent项目中,首次尝试构建工单分类器时的代码如下所示。
String result = chatClient.prompt("Analyze if incoming support ticket is about some kind of issue or is it a business request. "
+ "If it's a business request, the text will ask you to create something new or ask for estimation, if it's a support request "
+ "the text will say that something is not working properly, something is down or needs fixing. Answer with true if it's a "
+ "business request and false if it's a support request" + request.description())
.call()
.content();
它看起来与普通 API 类似,不同之处在于其契约由提示词定义,而非 JSON 模式。
在实际应用中它能产生准确的结果吗?很可能不能。它将持久性的指令与不可信的用户输入混放在同一字符串中,没有对任务内容进行分隔,还假设模型会返回 Java 能解析为布尔值的输出。而面向生产的版本则更像应用程序契约,将系统级指令与用户输入分开处理。
String result = chatClient.prompt()
.system("""
You classify incoming tickets for an L3 support workflow.
Return exactly one token: true or false.
Return true when the request asks for new functionality, estimation, planning, or project work.
Return false when the request reports broken, degraded, unavailable, or incorrect production behavior.
Do not explain your answer.
""")
.user(u -> u.text("""
Ticket title: {title}
Ticket source: {source}
Ticket description:
---
{description}
---
""")
.param("title", request.title())
.param("source", request.source())
.param("description", request.description()))
.call()
.content();
系统指令决定了行为规范与限制,而用户消息则包含动态输入内容。提示语最终会成为设计的一部分,记录相关假设、设定语气,并影响请求的解读方式。
这就催生了一门新的学科——提示工程。提示语不再只是被随意使用的字符串,而是一种需要版本控制、测试和维护的产物,只有这样处理才能确保系统的稳定性。Spring AI关于提示语及ChatClient的参考文档是接下来值得阅读的材料。
结构化输出
在将人工智能引入后端系统时,最早面临的挑战就是非结构化响应问题。模型会自然生成自由形式的文本,而企业级应用则需要结构化数据。
Spring AI通过将响应直接映射为Java对象来缩小这一差距。这样一来就无需手动解析,且在结果解读方面也减少了歧义产生的可能。
支持人员无需接收说明某操作失败的文字描述,而是可以通过Java记录来明确是哪个数据存储库和类可能与该问题相关。
@Service
@RequiredArgsConstructor
public class SupportAnalysisService {
private final ChatClient chatClient;
private final LogRetrievalTool logRetrievalTool;
public LogCauseReport analyzeLogs(SupportTicketRequest request) {
var converter = new BeanOutputConverter<>(LogCauseReport.class);
var format = converter.getFormat();
String response = chatClient.prompt("Analyze the support problem and provide solution, use available MCP tools to access logs. "
+ "Incoming support request description: " + request.description()
+ " \n provide response in this format " + format).tools(logRetrievalTool).call().content();
LogCauseReport report = converter.convert(response);
return report;
}
}
public record LogCauseReport(String causeOfProblem, String repoName, String className) {
}
在此,模型被引导生成可直接转换为强类型对象的响应,从而将集成方式从松散的文本处理转变为明确的契约机制。
其优势十分显著。非结构化输出会导致脆弱的解析过程,稍有表述变化就会出错;而结构化输出则可以像其他数据类型一样进行验证、记录和传递。官方文档中对这种转换方法有详细说明。
较新版本的 Spring AI 还提供了类型化实体映射功能,部分提供商还支持原生结构化输出,这两者都值得关注。在模型层面就强制执行契约,可减少应用程序需要处理的解析代码量。
工具调用
生成文本固然有用,但当人工智能能够通过检索数据、触发工作流或运行业务逻辑来与系统交互时,其真正价值才会显现。
工具调用功能通过暴露应用程序函数,使模型能够根据其推理结果来调用这些函数。不应将模型视为可直接访问基础设施的操作者,它只需请求某种功能,由应用程序决定该功能是否存在、参数如何验证、返回哪些数据以及执行过程如何被记录。
在项目中,SupportAnalysisService为日志分析步骤提供了LogRetrievalTool。定义工具时需要使用@Tool注解,其描述参数有助于模型判断何时调用该函数。
@Tool(name = "analyze_logs", description = "Analyzes provided logs and summarizes potential issues")
public LogCauseReport analyzeLogs(
@ToolParam(description = "Application name") String applicationName) {
var converter = new BeanOutputConverter<>(LogCauseReport.class);
var format = converter.getFormat();
String prompt = """
You are a support AI specialized in analyzing application logs to identify potential issues.
Given the application name "%s", analyze the relevant logs and provide a structured report.
Respond ONLY with a single JSON object that strictly matches this format:
%s
Do NOT return the schema itself, only valid JSON with values filled in.
""".formatted(applicationName, format);
String jsonResponse = chatClient.prompt()
.user(prompt)
.call()
.content();
return converter.convert(jsonResponse);
}
当模型判断需要更多信息时,它会调用相应函数,整合结果后继续生成内容。实际上,模型便成了系统功能的协调者。
然而,随着系统的扩展,硬编码的集成方式会逐渐成为限制,因为每新增一个工具都会增加模型与应用程序之间的耦合度。
利用 MCP 发现工具
模型上下文协议解决了这一问题。MCP 对工具和服务向模型暴露的方式进行了标准化,从而使这些工具能够被动态发现,无需嵌入到应用程序中。
MCP将固定的集成方式转变为动态生态系统,使得无需修改核心应用程序即可添加或修改功能,这是迈向可扩展的基于代理的架构的基础步骤。Spring AI的工具调用文档介绍了相应的编程模型与执行流程。当工具需要动态发现或必须在不同进程间共享时,MCP的支持便是顺理成章的解决方案。
检索增强生成
模型虽然功能强大,但却不了解您系统中的数据。除非提供相关信息,否则客户、产品及内部流程都将保持未知状态。
RAG通过在生成之前加入检索步骤来解决这一问题。相关的文档会从向量存储中提取出来并注入提示词中,从而使输出内容基于企业数据,如操作手册、生产日志和应用程序代码等。
对于支持人员而言,RAG会根据应用日志中提到的Java类,为特定的生产问题提供代码修复方案。
第一步是让SupportAgentController通过GithubRagService获取源代码片段,该服务会依据包含疑似代码库和类的LogCauseReport来工作。
String classCode = ragService.findRelevantClass(null, logCauseReport.repoName(), null, logCauseReport.className()).stream()
.map(SupportAgentController::stripMetadataLines)
.filter(content -> !content.isBlank())
.collect(Collectors.joining(System.lineSeparator() + System.lineSeparator()));
在第二步中,检索服务会构建元数据过滤器,并针对 Qdrant 向量存储执行相似度搜索。
SearchRequest.Builder searchRequestBuilder = SearchRequest.builder();
if (expressions != null) {
searchRequestBuilder = searchRequestBuilder.filterExpression(expressions.build());
}
Set<String> results = Objects.requireNonNull(vectorStore.similaritySearch(searchRequestBuilder.topK(10).build()))
.stream()
.map(Document::getFormattedContent)
.collect(Collectors.toSet());
第三步是将获取到的代码插入到请求修复方案的提示语中。
String prompt = """
You are a Senior Java Developer. You have the following class code:
%s
The code has the following problem: %s
Provide a detailed suggestion on how to fix the problem in the code.
Format your response in markdown with clear sections.
""".formatted(classCode, problemDescription);
String suggestion = chatClient.prompt(prompt).call().content();
其作用是将模型从通用助手转变为熟悉特定领域的助手。RAG主要能够减少幻觉现象,并使答案与内部知识保持一致,但代价是增加了复杂性。
它需要引入嵌入模型、向量数据库以及检索逻辑,而这些都需要进行管理和调优。在企业应用中,这种复杂性往往是不可避免的,因为答案质量取决于多个因素:输入数据的新鲜度、元数据的质量、分块策略、授权机制以及证据追踪。Spring AI关于RAG、向量存储和Qdrant的文档涵盖了本节中的相关概念。
可观测性、评估与运营控制
AI 的可观测性机制有所不同。传统的日志记录和监控方法基于确定性行为,而 AI 则需要追踪更多维度的数据。仅关注响应时间和错误率远远不够,团队还需要查看提示词、模型输出、令牌使用情况以及工具交互信息。
支持代理使用 Micrometer 作为其可观测性框架,用于捕获以下信息:
- 每个请求的标识符和上下文,如工单编号、调用者及源系统
- 模型提供方与名称、提示词版本、延迟时间、令牌使用量以及完成原因
- 结构化输出转换是否成功
- 每次工具调用,包括标准化后的参数、结果大小及延迟时间
- 检索到的存储库、文件、向量分数及数据块信息
- 工单的最终状态,以及人工审核员是否接受或修改了建议
这样的详细程度对于调试和优化至关重要,因为它能解释系统为何会呈现当前的行为。在生产环境中,敏感的提示语和输出不应被原样记录。一种更可靠的替代方案是采用结构化的事件日志,其中包含请求ID、模型ID、令牌数量以及策略执行结果。
输出评估则增加了另一层功能。系统可以根据预定义的标准来评估响应质量,而无需假设答案一定是正确的。这便实现了诸如评估器-优化器循环这样的模式,即在响应传递给用户之前对其进行优化。
它还改变了系统的演化方式。每当提示语、嵌入模型、分块方法或模型版本发生变化时,都可以将结果与已知案例进行对比,这样团队就能判断该变化是否真正提升了工作流程,而非仅仅依赖于少数出色的示例。
在将该助手开放给其他团队之前,项目团队考虑了以下一系列运营控制措施:
- 对模型、工具及向量存储的调用设置硬性超时时间
- 为每个请求及每个调用者设定令牌预算,以控制成本并减少故障风险
- 仅在安全且有限的情况下才允许重试
- 当模型、工具或Qdrant出现故障时具备备用方案
- 在生成提示语和日志之前先对敏感信息、凭证及个人数据进行脱敏处理
- 默认关闭内容日志记录,或仅在经过审批的访问控制下才允许记录
- 为提示语、生成结果、嵌入数据及工具输出设定明确的保留规则
- 针对转换失败、检索结果为空、工具错误以及审核人拒绝等情况提供仪表盘和警报功能
审核拒绝率最能反映实际使用情况。当工程师频繁重写或丢弃生成的推荐结果时,助手的工作量就会增加。生产系统应当从这一信号中学习。对所有内容进行自动微调并非正确方法,更好的途径是通过实际操作——利用可量化的证据来改进提示词、工具、检索机制以及相关策略。Spring AI的可观测性文档介绍了其内置的工具功能。
设计有界智能体工作流
将上述要素结合起来,就会出现一种新的模式:智能体系统。
传统应用遵循预定义的工作流,而智能体系统则通过观察、推理和行动的迭代循环来运行。它们能够保持状态、利用记忆,并动态决定采取何种行动。
状态、记忆和自主性成为核心考量因素,而这些都对应着Spring AI已具备的功能:
- 状态是指当前交互中已知的信息
- 记忆则是跨多次交互仍然保留的信息
- 工具决定了允许执行的操作
- 约束规则标明了哪些行为是被禁止的或需要确认的
- 终止条件规定了循环何时必须结束
这与传统的企业工作流相比,改变了应用程序的角色。系统不再需要定义每一个步骤,而是设定能力和约束,让模型在这些限制范围内协调行为。
在支持代理系统中,流程始于一个请求。代理会对该请求进行分析,调用相关工具并获取代码,随后起草解决方案并更新工单。它并非自主的生产操作工具,也不应变成此类工具。在金融科技领域,这一界限决定了代理在特定情境下可以采取哪些行动。在让模型修改工单之前,首先应确保其能够可靠地总结日志。
构建高效的代理系统十分困难,而 Spring AI 相关的文档是不错的起点。
从实验到企业级系统
人工智能并非仅仅是另一个功能特性,它为企业级系统增添了新的层次——这一层次具有概率性、动态性,并且与外部依赖紧密相连。若缺乏合理结构,这一层次很快就会导致系统行为脆弱且难以预测。
Spring AI提供了一种集成方式,能够在保持使企业系统具备可靠性的工程实践的同时引入人工智能技术。熟悉的抽象概念和明确定义的集成模式让团队能够在不放弃明确契约、受控交互以及可预测行为的前提下应用人工智能。
因此,这种变革并不仅仅是添加人工智能功能,而是让系统不断进化,从而使人工智能成为架构中一个有用、受管控且可观察的部分。下一代企业应用将取决于它们集成人工智能的能力,而其使用方式则相对次要。
相关阅读
- 实用笔记:RAG已过时。LLM Wiki——Andrej Karpathy的新理念才是未来方向 —— 对《实用笔记:RAG已过时。LLM Wiki——Andrej Karpathy的新理念才是未来方向》的详细操作指南:包括合同、检查项以及供正在开发RAG系统的团队使用的代码模板。
- RAG项目不应是单个庞大的Python文件:可扩展的文件夹结构 —— 将演示用的RAG应用拆分为数据导入、分块处理、嵌入生成、向量存储、检索、提示词设计、LLM客户端、API接口、测试及日志记录等模块,使其能够超越初期测试阶段并实现进一步发展。