打造支持上下文与历史对话的 Excel GPT
在上一篇文章中,我们实现了通过调用 AI 接口生成指定内容的 Excel,具备了根据输入生成预览和下载的功能。
今天我们直接做个大的,创建一个拥有用户交互界面的 Excel GPT,并且实现支持上下文和历史对话记录的强大功能!
一、功能升级
相比上次,这次更新主要引入了数据库作为持久层,用于支持不同历史对话的切换以及当前对话的上下文记录。同时,添加了 router 路由管理,采用 nodemon 实现热更新,并且在服务端实现了加载静态文件的功能。考虑到idea到项目的快速实现,没有花费过多时间精力进行前后端分离,后续项目会考虑使用 NextJS 框架。
二、参考
上下文硬盘缓存技术
DeepSeek API 提供了上下文硬盘缓存技术,并且对所有用户默认开启,用户无需修改代码即可使用。每一个用户请求都会触发硬盘缓存的构建。当后续请求与之前的请求在前缀上存在重复时,重复部分将从缓存中拉取,计入 “缓存命中”。 例如:
第一次请求
messages: [
{"role": "system", "content": "你是一位资深的财报分析师..."}
{"role": "user", "content": "<财报内容>\n\n请总结一下这份财报的关键信息。"}
]
第二次请求
messages: [
{"role": "system", "content": "你是一位资深的财报分析师..."}
{"role": "user", "content": "<财报内容>\n\n请分析一下这份财报的盈利情况。"}
]
在上述例子中,两次请求的相同前缀部分(即 system 消息 + user 消息中的 <财报内容>),在第二次请求时会被计入 “缓存命中”。这一技术大大提高了请求处理的效率,减少了与 AI 模型的重复交互,提升了用户体验。
JSON Output
deepseek官方的JSON规范化输出我在调试的过程中经常出现问题,因为有写AI生成内容为空就会跑出异常,所以我还是接续了上次的逻辑,在提示词里面做文章。
多轮对话
DeepSeek /chat/completions API 是一个“无状态” API,即服务端不记录用户请求的上下文,用户在每次请求时,需将之前所有对话历史拼接好后,传递给对话 API。
三、数据库(DB)设计
我选用了 prisma 作为 ORM 工具,因为真的快,只需编写好 model 并执行几个命令,即可完成整个配置。数据库的设计结构如下:
四、项目结构
经典的 MVC 结构,基于 koa 服务搭建。项目结构如下:
excel-generator/
├── src/
│ ├── controller/ # 控制器层,负责处理请求和响应
│ ├── service/ # 服务层,封装业务逻辑
│ ├── config/ # 配置,存放各种配置信息
│ ├── view/ # 前端页面,提供用户交互界面
│ └── index.ts # 入口文件,启动整个应用
五、提示词模板(Prompt Template)配置
提示词模板的配置对于与 AI 模型的交互至关重要。我们定义了两个主要的提示词模板:
export const EXCEL_GENERATOR_TEMPLATE = {
SYSTEM_PROMPT: `你是一个经验丰富的 Excel 生成器。用户会给你一个生成 Excel 的需求,你需要根据需求返回一个 JSON 格式的模板。JSON 模板的结构如下:
{
"columns": [{"header": "列标题1", "key": "key1"}, ...],
"data": [{"key1": "值1", "key2": "值2"}, ...]
}
只要纯json数据回应,不要包含其他任何内容`,
EXCEL_TRANSFORM_PROMPT: `根据上传的Excel数据和用户需求生成新的数据结构,按照模板:
JSON 模板的结构如下:
{
"columns": [{"header": "列标题1", "key": "key1"}, ...],
"data": [{"key1": "值1", "key2": "值2"}, ...]
}
只要纯json数据回应,不要包含其他任何描述和说明的文字内容。`,
};
SYSTEM_PROMPT:这个提示词告诉 AI 模型,它的角色是一个 Excel 生成器,并且明确了返回的 JSON 格式模板结构。为了让模型生成的内容能够直接被我们的程序解析和使用,避免生成不必要的文本描述,方便后续处理。
EXCEL_TRANSFORM_PROMPT:用于在已有 Excel 数据的基础上,根据用户需求进行数据结构的转换。同样规定了返回的 JSON 格式,目的是确保 AI 模型的输出格式统一且符合我们的处理要求
六、核心函数功能
1. generateAIResponse 函数
private async generateAIResponse(prompt: string, sessionId: number) {
try {
// 获取当前会话的历史消息
const data = await prisma.message.findMany({
where: {
sessionId: sessionId,
},
orderBy: {
createdAt: "asc",
},
});
const messages: Array<ChatCompletionMessageParam> = [
{
role: "system",
content: EXCEL_GENERATOR_TEMPLATE.SYSTEM_PROMPT,
},
...data.map(
(msg): ChatCompletionMessageParam => ({
role: msg.role as "user" | "assistant" | "system",
content: msg.content,
})
),
{
role: "user",
content: prompt,
},
];
const completion = await this.openai.chat.completions.create({
model: "deepseek-chat",
messages,
temperature: 0,
// response_format: { type: "json_object" },
});
const responseContent = completion.choices[0]?.message?.content?.trim();
if (!responseContent) {
throw new Error("AI 响应内容为空");
}
return responseContent;
} catch (error) {
console.error("生成 AI 响应失败:", error);
throw new Error("生成回复失败,请稍后重试");
}
}
功能:这个函数用于生成 AI 响应。首先从数据库中获取指定会话 ID(sessionId)的所有历史消息,然后将系统提示词、历史消息以及当前用户输入的提示词(prompt)组合成一个完整的消息数组,发送给 AI 模型(deepseek-chat)进行处理。最后,从 AI 模型的响应中提取内容并返回。
2. transformExcelData 函数
async transformExcelData(originalData: any, prompt: string) {
try {
const messages: Array<ChatCompletionMessageParam> = [
{
role: "system",
content: EXCEL_GENERATOR_TEMPLATE.EXCEL_TRANSFORM_PROMPT,
},
{
role: "user",
content: `原始数据: ${JSON.stringify(
originalData
)}\n用户需求: ${prompt}`,
},
];
const completion = await this.openai.chat.completions.create({
model: "deepseek-chat",
messages: messages,
temperature: 0,
});
if (!completion.choices[0]?.message?.content) {
throw new Error("AI响应内容为空");
}
const cleanContent = this.cleanJsonString(
completion.choices[0].message.content
);
return JSON.parse(cleanContent);
} catch (error) {
console.error("转换Excel数据失败:", error);
throw new Error("转换Excel数据失败,请稍后重试");
}
}
功能:它的作用是根据上传的原始 Excel 数据(originalData)和用户需求提示词(prompt),通过调用 AI 模型(deepseek-chat)生成新的数据结构。它将系统提示词和包含原始数据与用户需求的消息发送给 AI 模型,获取响应后,对响应内容进行清理和解析,最终返回解析后的新数据结构。
七、页面UI交互设计
我参照了GPT和Gemini的经典左右两栏式设计和页面风格,回复用table view展示,引入之前的xlsx库cdn来做表格内容下载。
- 页面结构
app-container/
├── sidebar/ # 左侧会话列表
│ ├── new-chat-button # 新建会话按钮│ └── chat-list # 会话列表
└── main-content/ # 主要内容区 ├── chat-history # 聊天记录
└── input-container # 输入区域
└── toolbar # 工具栏(上传按钮、输入框、发送按钮)
- 核心功能流程
- 初始化流程
initializeChat() {
1. 获取用户会话列表 GET /sessions
2. 渲染会话列表 renderSessions()
3. 获取当前会话消息 GET /sessions/${id}/messages
4. 渲染消息列表 renderMessages()
}
- 发送消息流程
sendMessage() {
if (有上传文件) {
1. 上传文件 POST /upload-excel
2. 保存用户消息 POST /messages
3. 保存AI响应 POST /messages
4. 更新UI addMessage()
} else {
1. 发送普通消息 POST /messages
2. 更新UI addMessage()
}
}
3. 页面展示
- 对话session切换:
- 上传文件:
- 回复展示:
- 下载验收:
summarize
这个项目大部分的时间都在调输入输出,在数据转换上花的功夫比较多,不过还是一个晚上搞定了,因为有ai工作流的加持,很多基本重复的编码工作都被省去了,这个项目之后可能也会继续增加别的功能并做重构,但核心功能都已经差不多了。我之后可能会做工作内容相关的生成思维导图,或者其他AIGC的试验和玩具,欢迎一起交流哈~
微信公众号:冻柠葡萄呗