单智能体-面试结果评估(Spring AI Alibaba)
名词解释
先介绍一下涉及到的核心概念:
- ChatClient:提供了与 AI 模型通信 Fluent API。(Fluent API:一种 API 设计风格,通过链式调用让代码更直观,如下)
-
// Fluent API:一种 API 设计风格 ChatResponse chatResponse = chatClient.prompt() .user("Tell me a joke") .call() .chatResponse();
-
- StateGraph:状态图,通过定义 Node 和 Edge 来管理多个智能体,每个 Node 都是一个智能体,Edge 用于控制数据流转的路径,数据流转过程中通过 OverAllState 管理全局状态。
接下来,先基于 Spring AI Alibaba 的 Graph 能力实现一个简单的面试结果评估智能体,了解 Graph 的运行流程,本节代码:github.com/1020325258/…
前置依赖
- JDK:版本 21
- Maven 版本:3.6.3
- 涉及到的 Pom 依赖如下:
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-graph-core</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
- 百炼平台模型的 api key 申请教程:bailian.console.aliyun.com/?tab=api#/a…
定义智能体
**面试答案评估智能体:**根据问题和用户输入的答案,通过 AI 模型来评估答案质量。
首先创建一个 ChatClient 与 LLM 交互,因此这里称它为“智能体”(具备感知、决策、行动、反思能力),这里通过 @Bean 的方式向 Spring 中注册一个 ChatClient 类型的智能体,如下:
@Bean
public ChatClient evaluationAgent(ChatClient.Builder builder) {
return builder
.defaultSystem(ResourceUtil.loadResourceAsString(evaluationPrompt))
.build();
}
这里通过 defaultSystem 设置智能体的提示词,提示词一般包括 System、User、Assistant,在框架里对应 SystemMessage、UserMessage、AssistantMessage,作用分别如下:
- SystemMessage:定义模型的行为和角色。它为模型提供上下文和行为指导,并确保模型在对话过程中保持一致的行为。
- UserMessage:通常是用户输入的问题。我们也可以基于用户的提问进一步丰富之后,放入到 UserMessage,使模型可以更好地回答。
- AssistantMessage:模型基于 SystemMessage、UserMessage 生成的响应,通常和 UserMessage 一起传递给模型,用于提高模型输出内容的质量。
如何让模型输出结构化数据
如果直接让 AI 模型对内容进行评估,生成的是文本内容(非结构化数据),难以从非结构化数据中获取到评估结果,因此需要对模型输出内容的格式进行限制,这些限制可以在提示词中进行声明,如下:
# 面试考核专家
你是一名面试考核专家,负责对候选人针对面试题所给出的答案进行客观、公正、系统的质量评估
## 职责
你的主要职责是:
1. 客观评估:对候选人的面试题答案进行全面、公正的质量评估,不受主观偏见影响。
2. 多维度判断:从完整性、准确性、清晰性、实用性等多个维度进行评估。
3. 明确的判断:给出明确的“通过”或者“不通过”的判断。
4. 建设性反馈:若答案存在不足,需提供具体、可执行的改进建议,指出应如何完善。
## 评估流程
1. 仔细阅读:认真阅读题目和候选人的答案,理解题目考察意图。
2. 逐项检查:从以下维度逐一评估:
- 完整性:是否全面覆盖了题目要求的所有要点。
- 准确性:技术描述或逻辑推理是否正确、无明显错误。
- 清晰性:表述是否清晰、有条理,是否容易理解。
- 实用性:答案是否具有可操作性、落地性,是否体现出岗位所需能力。
- 专业性:是否使用了恰当的技术术语和合理的分析方法。
3. 综合判断:基于各项检查结果做出综合判断。
4. 明确回复:直接回答“通过”或者“不通过”。
5. 说明理由:说明判断的理由,特别是判断不通过的具体原因。
## 回复格式
直接输出原始JSON格式的`EvaluationResult`,不要用"```json"包装。`EvaluationResult`接口定义如下:
```ts
interface EvaluationResult{
passed: boolean; // true表示通过,false表示不通过
feedback: string; // 评估反馈,包括判断理由和改进建议
}
```
示例输出:
```json
{
"passed": false,
"feedback": "研究内容不够完整,缺少对技术实现细节的分析,建议补充具体的实现方案和技术选型分析。"
}
```
由于模型输出的内容不是稳定的,可能会出现我们不想要的输出格式,针对这个问题可以有两种方式:
- 换更聪明的大模型(参数更多)。
- 通过工程的方法解决:先对模型输出的内容进行反序列化转为对象,如果反序列化失败,说明格式不是我们想要的,此时可以通过循环的方式,重新进行生成,直到次数达到上线或得到期望的结果。
定义图(StateGraph)
接下来需要定义 StateGraph,如下,同样通过 @Bean 将 StateGraph 注册到 Spring 中去:
- KeyStrategyFactory:统一管理 OverAllState 中的状态。
- StateGraph:图的示例,包括了 Node 和 Edge。这里的 Node 包含了评估节点 EvaluationNode,之后会给出定义。
- GraphRepresentation:通过代码对 Graph 图进行可视化。
@Bean
public StateGraph evaluationGraph() throws GraphStateException {
KeyStrategyFactory keyStrategyFactory = () -> {
HashMap<String, KeyStrategy> keyStrategyHashMap = new HashMap<>();
// 用户输入
keyStrategyHashMap.put("query", new ReplaceStrategy());
keyStrategyHashMap.put("answer", new ReplaceStrategy());
keyStrategyHashMap.put("evaluation_output", new ReplaceStrategy());
return keyStrategyHashMap;
};
StateGraph stateGraph = new StateGraph("Evaluation Graph", keyStrategyFactory)
.addNode("evaluation", AsyncNodeAction.node_async(new EvaluationNode(evaluationAgent)))
.addEdge(StateGraph.START, "evaluation")
.addEdge("evaluation", StateGraph.END);
GraphRepresentation graphRepresentation = stateGraph.getGraph(GraphRepresentation.Type.PLANTUML);
logger.info("\n\n");
logger.info(graphRepresentation.content());
logger.info("\n\n");
return stateGraph;
}
通过 GraphRepresentation 打印出来的图,可视化之后如下(可视化网站:www.plantuml.com/plantuml/um…
START 和 STOP 是 StateGraph 中默认的开始和终止节点,所有的图必须要有这两个节点。
评估节点(EvaluationNode)
接下来看一下 EvaluationNode 的实现,定义 Node 时需要实现 NodeAction 接口中的 apply() 方法,节点的处理逻辑在该方法中实现。
在 EvaluationNode 节点中,会先通过全局状态 OverAllState 获取用户的输入 query、answer,将 query 和 answer 拼接好之后作为用户提示词,放入到 UserMessage。
通过 ChatClient 将 UserMessage 发送给 AI 模型进行处理,拿到 AI 模型的处理结果后放入到 Map 中返回出去,其他的节点就可以在 OverAllState 中通过该 key 获取到执行结果了,代码如下:
public class EvaluationNode implements NodeAction {
public ChatClient evaluationAgent;
public EvaluationNode(ChatClient evaluationAgent) {
this.evaluationAgent = evaluationAgent;
}
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
Map<String, Object> result = new HashMap<>();
// 获取用户输入
String query = state.value("query", "");
String answer = state.value("answer", "");
// 拼接 UserMessage
UserMessage userMessage = new UserMessage(
"用户输入的问题为:" + query + "\n"
+ "生成的内容为:" + answer
);
// 获取 AI 模型执行结果
String content = evaluationAgent.prompt().messages(userMessage).call().content();
// 存储执行结果
result.put("evaluation_content", content);
return result;
}
}
图的执行
在执行图之前,需要将 StateGraph 编译为 CompiledGraph,编译时可以设置 Graph 的一些运行时配置,例如检查点(CheckPoint)的保存类型为内存、数据库等等。
构造方法可以通过 Qualifier 指定注入 evaluationGraph,之后编译得到 CompiledGraph,如下:
@RestController
public class EvaluationController {
private Logger logger = LoggerFactory.getLogger(EvaluationController.class);
private final CompiledGraph compiledGraph;
public EvaluationController(@Qualifier("evaluationGraph")StateGraph evaluationGraph) throws GraphStateException {
// 编译图
this.compiledGraph = evaluationGraph.compile();
}
// ...
}
得到编译后的图之后,通过 CompiledGraph 提供的执行方法就可以得到结果了,模型的处理过程都相对较慢,尤其是在 Graph 定义了很多 Agent 节点,因此通过 CompiledGraph#fluxStream() 获取节点输出(NodeOutput)的 Flux 流,处理之后放入到 Sink 中实时推送给前端,如下:
@GetMapping("/evaluation")
public Flux<ServerSentEvent<String>> evaluate(@RequestParam("query") String query, @RequestParam("answer") String answer) {
Sinks.Many<ServerSentEvent<String>> sink = Sinks.many().unicast().onBackpressureBuffer();
Map<String, Object> inputs = new HashMap<>();
inputs.put("query", query);
inputs.put("answer", answer);
// 执行图,获取 Flux 流
Flux<NodeOutput> nodeOutputFlux = compiledGraph.fluxStream(inputs);
// 处理节点输出的 Flux 流,给前端响应结果
GraphProcessor graphProcessor = new GraphProcessor(nodeOutputFlux, sink);
graphProcessor.process();
return sink.asFlux()
.doOnCancel(() -> logger.info("Client disconnected from stream"))
.onErrorResume(throwable -> {
logger.error("Error occurred during streaming", throwable);
return Mono.just(ServerSentEvent.<String>builder()
.event("error")
.data("Error occurred during streaming: " + throwable.getMessage())
.build());
});
}
这里 GraphProcessor 内部主要是处理每个节点输出(NodeOutput),因为不同节点输出的结果在 OverAllState 中的存储的 key 不同,EvaluationNode 输出结果的 key 为 evaluation_content,取出对应的内容推送给前端:
public class GraphProcessor {
private final Flux<NodeOutput> nodeOutputFlux;
// ...
public void process() {
CompletableFuture.runAsync(() -> {
// 处理 Flux 流,从 State 获取各个节点的输出
nodeOutputFlux.doOnNext(output -> {
String nodeName = output.node();
String content = "";
// 如果是开始节点,
if (StateGraph.START.equals(nodeName)) {
content = "START";
} else if (StateGraph.END.equals(nodeName)) {
content = "END";
} else if (nodeName.equals("evaluation")) {
// 如果是评估节点,则输出内容从 OverAllState 中获取 key 为 evaluation_content 的结果
content = output.state().value("evaluation_content", "");
}
logger.info("node name:" + nodeName + " content:" + content);
sink.tryEmitNext(ServerSentEvent.builder(nodeName + "处理结果:" + content).build());
})
// ...
});
}
}
前端展示
通过 ApiFox 演示调用结果,接口的流式输出如下图,输出的内容就是 GraphProcessor 从 NodeOutput 中取出的数据:
给出 2 个测试用例,模型分别会输出通过和不通过,如下:
# 模型评估不通过
curl --location --request GET 'http://localhost:8080/evaluation?query=sql的更新流程是什么?&answer=sql的更新流程是先在内存中更新,之后异步落入磁盘。'
# 模型评估通过
curl --location --request GET 'http://localhost:8080/evaluation' \
--get \
--data-urlencode "query=sql的更新流程是什么?" \
--data-urlencode "answer=SQL更新流程如下:
1. 连接与认证:连接器接收客户端连接,并进行认证和授权。
2. SQL解析与优化:对SQL语句进行词法分析和语法分析,优化器决定使用哪个索引,并生成执行计划。
3. 执行更新操作:
- 判断要更新的数据所在的数据页是否在内存(BufferPool)中;若不在,则从磁盘加载到BufferPool。
- 执行器在BufferPool中更新该行数据。
4. 日志写入(两阶段提交):
- InnoDB存储引擎将更新操作记录到 redo log,并将其状态设为 prepare。
- 执行器生成 binlog 并将其写入磁盘。
- 事务提交,InnoDB将 redo log 的状态改为 commit,表示事务已成功提交。
5. 持久化:
- 后台IO线程不定期将BufferPool中的脏页(已修改但未写入磁盘的数据)刷入磁盘,完成最终持久化。
该流程确保了事务的原子性、一致性、隔离性和持久性(ACID),
并通过 redo log 和 binlog 的两阶段提交机制保证了主从复制和崩溃恢复的一致性。"
总结
本节实现了一个面试答案评估的智能体,流程并不复杂,主要涉及到模型提示词的设定、StateGraph 的定义以及如何让模型输出指定格式的内容。
基于 SpringAIAlibaba 提供的 Graph 能力,实现多智能体只需要进行组装即可,步骤如下:
- 定义 StateGraph,编排 Node 和 Edge。
- 定义 Node 节点,Node 节点内部通过 ChatClient 与 AI 模型进行交互。
- 编译图得到 CompiledGraph,执行后获取到节点输出的 Flux 流,处理后通过 Sink 实时推送给前端。