项目背景
最近做了一个大模型demo,自动生成sql,获取数据然后在前端页面画图。
这个demo的核心就是大模型生成sql的准确性,后面的都是常规前后端。
deepseek的优秀能力
首先用deepseek 试了一下,deepseek R1得益于他模型能力的强悍,出来的sql基本上都是能跑出数据,不会报错的,而且基本上也都是对的。
但是他的缺点就是,接口调用太慢了,一个接口基本上都至少要两三分钟才能出结果,如果要先后调用两次或者多次的话,交互体验太不友好了。
官方的接口慢,其实也有快的接口,阿里百炼提供的R1的接口会快很多,基本上一分钟左右就能出结果,其实是可以接受的。
其他模型的欠缺与优点
deepseek 的能力优秀,生成的sql 质量很高,跑起来不会报错,查询的数据也基本上是用户想看的。但是其他模型的能力没有R1 这么强,生成的sql跑起来经常直接报错,但是他们这些模型也有自己的优点,那就是快,基本上3s~7s就能出结果,因为少了推理。
那么有没有办法能利用起来这些模型,生成的sql又快,跑起来还不报错。
retry
其实和我们平时让大模型写代码的思路一样,先写一版,如果报错了,就直接把报错信息丢给大模型,让它对报错进行修正。
当大模型返回结果的时候,我们把sql直接运行起来,把这个过程try catch一下,如果报错了,把报错添加上下文中,再丢给大模型,让他再出一版,直到不报错。
实践
retry 的过程为了防止大模型实在太呆了,很多次retry都还是报错,应该设置一个重试的最大次数。
那么如果达到最大次数结果还是错的,我认为应该有两种方案:
- 直接返回错误到前端,让用户修改问题的描述,然后重试
- 有一个兜底的模型,当达到最大重试次数还是不行之后,自动切换成deepseek 获取sql
我试了一下智谱 的模型GLM-4-Air,最多不超过一半的问题能直接给出正确的结果,对于出问题的结果,大部分也就重试个一次两次就能成功,很少的三次还不成功的,基本上要5次以上。
也就是说对于大部分的问题,这种方案总时间还是比直接用deepseek 快很多的,我认为retry 方案再配合上deepseek进行兜底的方案的可行性是非常高的。
代码
下面展示了一个基于langchain.js的基本封装,历史记录就简单用一个数组进行维护了,没有用到内置的history memory模块。
在调用的时候,只要调用invokeWithRetry方法,传入用户的问题,和sql语句查询函数,以及最大重试次数。
当然,这里的callback不仅仅可以是sql查询,也可以是任何其他任务。
也就是说,这种方法方案的使用场景很多。
js
import { RunnableSequence } from '@langchain/core/runnables'
import { PromptTemplate } from '@langchain/core/prompts'
class LLM {
constructor(model, options = {}) {
// 配置参数
this.maxHistory = options.maxHistory || 5
this.history = []
this.systemLogs = []
// 构建处理流水线
this.chain = RunnableSequence.from([
this._formatInput.bind(this),
this._buildPromptTemplate(),
model.bind({ responseType: 'text' })
])
}
// 核心调用方法(带自动重试)
async invokeWithRetry(prompt, callback, maxRetries = 7) {
let retryCount = 0
let lastError = null
while (retryCount <= maxRetries) {
try {
// 执行主调用流程
console.log(`执行了${retryCount + 1}次`)
const response = await this.invoke(prompt)
// 执行回调并返回结果
const result = await callback(response)
return result
} catch (error) {
lastError = error
console.log(error)
this._recordRetryError(prompt, error, retryCount)
if (retryCount >= maxRetries) break
// 指数退避等待
await this._sleep(1000 * Math.pow(2, retryCount))
retryCount++
}
}
// 重试耗尽后的处理
this._logFatalError(prompt, lastError)
throw this._formatFinalError(lastError, maxRetries)
}
// 基础调用方法
async invoke(input) {
try {
const response = await this.chain.invoke({ input })
this._saveHistory({ role: 'user', content: input })
this._saveHistory({ role: 'ai', content: response })
return response
} catch (error) {
this._logSystemError(input, error)
throw error
}
}
// 历史记录管理
_saveHistory(entry) {
this.history.push(entry)
// 自动清理旧历史(保留最近N轮对话)
if (this.history.length > this.maxHistory * 2) {
this.history = this.history.slice(-this.maxHistory * 2)
}
}
// 重试错误记录(带上下文信息)
_recordRetryError(prompt, error, retryCount) {
this._saveHistory({
role: 'system',
content: `重试#${retryCount}: ${error.message}`
})
this.systemLogs.push({
timestamp: new Date().toISOString(),
type: 'RETRY_ERROR',
prompt,
retryCount,
error: error.stack
})
}
// 其他辅助方法
_formatInput({ input }) {
return {
formattedHistory: this.history
.slice(-this.maxHistory * 2)
.map((e) => `${e.role}: ${e.content}`)
.join('\n'),
currentInput: input
}
}
_buildPromptTemplate() {
return PromptTemplate.fromTemplate(`
对话上下文(最多${this.maxHistory}轮):
{formattedHistory}
最新输入:
{currentInput}
请生成响应:
`)
}
_sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
_logFatalError(prompt, error) {
this.systemLogs.push({
timestamp: new Date().toISOString(),
type: 'FATAL_ERROR',
prompt,
error: error.stack
})
}
_formatFinalError(error, maxRetries) {
return new Error(`
请求在${maxRetries}次重试后失败。
最后错误:${error.message}
历史记录:${JSON.stringify(this.history.slice(-2))}
`)
}
}
export default LLM