剧本聊天 Agent
| 这个作业属于哪个课程 | Class56-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/16760 |
| 这个作业的目标 | 完成可调用工具的 Agent |
| 学号 | 3124004056黄凯祺 |
| github链接 | https://github.com/CageHuang1220/AIScriptAgent |
剧本聊天 Agent 说明文档
1. 需求描述
本项目实现一个基于 Spring AI 的轻量级剧本聊天 Agent,满足作业中“智能体必须能:自然语言对话(能说)、执行操作或调用工具(会做)”两项能力要求。
具体需求如下:
- 剧本上传:用户上传一个
TXT格式的剧本文件,系统解析文件名、标题、行数、字数、角色列表、幕/场列表等基础信息。 - 自然语言对话:用户能够围绕该剧本与 Agent 自由对话,Agent 可以回答剧情问题,也可以扮演剧本中的角色与用户对戏。
- 工具调用:Agent 在需要精确信息时,能够主动调用工具检索剧本原文,而不是仅凭生成内容“凭感觉”回答。工具包括剧本概览、角色列表、关键词检索、按行号读取。
- 历史记录:多轮对话需要具备上下文记忆能力;历史记录 Advisor 的缓存使用 Java
Map实现。 - 轻量化:尽可能只使用 Java 与 Spring 生态,不引入 SQL 数据库、向量数据库、消息队列等中间件;剧本与历史记录均保存在进程内存中。
- 前端:提供一个可直接使用的网页界面,用于上传剧本、查看剧本信息、进行对话和清空历史。
2. 业务流描述
整体业务流如下:
-
上传剧本
- 用户在网页左侧选择或拖拽一个
TXT文件。 - 前端通过
POST /api/scripts/upload将文件上传到后端。 - 后端读取文本,解析出标题、角色、幕/场等元数据,生成一个唯一的
sessionId,并将剧本保存在内存的ScriptStore中。
- 用户在网页左侧选择或拖拽一个
-
发起对话
- 前端将
sessionId和用户输入发送到POST /api/chat。 - 后端组装系统提示词:把 Agent 人设、工作规则以及“剧本全文”注入 Prompt(纯 Prompt 方案,不进行向量化检索)。
MessageChatMemoryAdvisor从基于Map的历史缓存中取出当前会话的历史消息,拼接到本次 Prompt 中。- 模型根据用户问题决定是直接回答,还是调用工具。
- 前端将
-
工具调用
- 当问题涉及“有哪些角色”“某句台词在哪里”等精确信息时,模型发起 function calling。
- Spring AI 将工具调用分发给
ScriptTools,并通过ToolContext把sessionId传给工具。 - 工具从
ScriptStore中取出对应剧本进行检索,返回结果;模型再根据工具结果生成最终回答。
-
记忆回写
- 本轮对话结束后,Advisor 将用户消息与助手回复写入
MapChatMemory(系统提示不缓存,避免剧本全文在多轮中重复堆积)。 - 下一轮对话时自动携带历史上下文,实现多轮记忆。
- 本轮对话结束后,Advisor 将用户消息与助手回复写入
-
会话管理
- “清空对话”只清除历史缓存,保留剧本。
- “更换剧本”删除整个会话(剧本与历史),回到初始状态。
3. 实现说明
3.1 技术选型
| 部分 | 选型 |
|---|---|
| 后端框架 | Spring Boot 3.4.5 |
| AI 框架 | Spring AI 1.0.0,spring-ai-starter-model-openai |
| 模型接口 | OpenAI 兼容接口,默认 DeepSeek,可切换 OpenAI / 通义千问 |
| 历史缓存 | 自定义 MapChatMemory,底层为 ConcurrentHashMap |
| 剧本存储 | 自定义 ScriptStore,底层为 ConcurrentHashMap |
| 前端 | 原生 HTML + CSS + JavaScript,由 Spring Boot 静态资源托管 |
3.2 项目结构
src/main/java/com/example/scriptagent/
├── ScriptAgentApplication.java # 启动类
├── config/ChatConfig.java # ChatClient 与 Advisor Bean 装配
├── memory/MapChatMemory.java # 基于 Map 的历史记录缓存
├── script/
│ ├── ScriptDocument.java # 剧本解析与元数据
│ ├── ScriptSession.java # 会话
│ └── ScriptStore.java # 内存剧本仓库
├── service/ScriptChatService.java # 对话编排:Prompt + 记忆 + 工具
├── tool/ScriptTools.java # @Tool 工具集
└── web/
├── ChatController.java
├── ScriptController.java
├── GlobalExceptionHandler.java
└── dto/ # 请求响应对象
3.3 核心实现
历史记录 Advisor 使用 Map 缓存
MapChatMemory 实现了 Spring AI 的 ChatMemory 接口,内部只用一个 ConcurrentHashMap<String, List<Message>> 保存各会话历史,不依赖任何数据库。
@Component
public class MapChatMemory implements ChatMemory {
private final Map<String, List<Message>> conversations = new ConcurrentHashMap<>();
// add / get / clear 三个方法围绕 conversations 实现
}
ChatConfig 将它与 Spring AI 官方的 MessageChatMemoryAdvisor 绑定:
MessageChatMemoryAdvisor.builder(chatMemory).build();
每次请求通过 .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId)) 指定当前会话,Advisor 就按会话从 Map 中读取和写入历史。
纯 Prompt 注入剧本
ScriptChatService 将剧本全文写入系统消息(超过 script-agent.max-prompt-chars 时截断,并提示模型用工具检索其余内容)。整个流程不涉及向量化和 RAG,符合“纯 Prompt”的要求。
工具调用(会做)
ScriptTools 使用 Spring AI 的 @Tool 注解声明工具,共 4 个:
| 工具 | 作用 |
|---|---|
scriptOverview |
返回剧本概览(标题、行数、字数、角色、幕/场) |
listCharacters |
列出所有角色 |
searchScript |
按关键词检索台词并返回行号 |
getLine |
按行号读取一行原文 |
工具通过 ToolContext 获取 sessionId,从而定位到当前会话的剧本,避免让模型直接传递会话 ID:
@Tool(description = "...")
public String searchScript(@ToolParam(description = "...") String keyword, ToolContext toolContext) {
Object sessionId = toolContext.getContext().get("sessionId");
// 从 ScriptStore 中取剧本并检索
}
3.4 接口列表
| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/api/scripts/upload |
上传 TXT 剧本 |
GET |
/api/scripts/{sessionId} |
查询剧本信息 |
POST |
/api/chat |
对话 |
DELETE |
/api/sessions/{sessionId}/history |
清空历史 |
DELETE |
/api/sessions/{sessionId} |
删除会话 |
4. 使用示例(功能演示)
4.1 启动
$env:JAVA_HOME = "D:\Program Files\jdk17"
$env:AI_API_KEY = "你的 API Key"
mvn spring-boot:run
4.2 上传
在左侧上传区选择 sample-script.txt,界面展示:
- 标题:《深夜食堂·一碗阳春面》
- 行数、字数
- 角色:老陈、小雨、阿婆
- 幕/场:第一幕 雨夜、第二幕 打烊前
4.3 对话示例
问(自然语言对话)
这个剧本讲了什么?
答
这个剧本讲的是雨夜里的深夜食堂,老板老陈、赶稿的小雨和常客阿婆之间围绕一碗阳春面展开的温暖故事。
问(工具调用)
小雨说过哪些台词?
此时 Agent 会调用 searchScript 工具检索原文,再整理回答:
小雨的台词有:第 7 行“老板,来一碗阳春面,多放葱花。”、第 8 行“加班赶稿子,饿得前胸贴后背了。”、第 14 行“老板,你在这条街开了多少年了?”、第 17 行“那我以后常来,就当是给自己加个深夜食堂。”
问(角色扮演)
请扮演阿婆,和我打个招呼。
答
阿婆:孩子,又到饭点啦?再忙也要吃口热乎的,别把胃熬坏咯。
4.4 多轮记忆演示
用户:老陈开了多少年店?
Agent:二十年。
用户:那小雨呢,她为什么这么晚才来?
Agent:因为她加班赶稿子,饿得前胸贴后背了。
第二轮问答中,Agent 能结合上一轮“老陈开店二十年”的上下文继续对话,体现历史记录 Advisor 的作用。
5. 心得
这次作业让我真正理解了 Spring AI 中 ChatClient、Advisor 与 Tool 三者如何协作。最深的体会是:记忆不要为了“记住”而记住,像剧本全文这种每轮都要用的内容,放在系统 Prompt 里重新注入、而不写入历史缓存,才能避免上下文越滚越大。工具调用则让我看到“会做”的边界——模型只是表达“我想调用哪个工具、传什么参数”,真正的执行还是在 Java 代码里。

浙公网安备 33010602011771号