最近在写个人博客,想着现在AI发展这么快速,就打算在博客里集成一个AI。
模型选用的智谱GLM的那个免费模型,而前端界面就选用的antd的pro-chat。 当然自己也写了一个单独的页面,但是功能上有点问题,所有这里就不展示
我是通过nest.js调用的大模型的接口,代码如下:
controller层
@Post('liu')
async createDialogueByliu(
@Body() body: { messages: MessageOptions[] },
@Res() res: any,
) {
console.log(body.messages);
try {
const dataStream = await this.aimodelService.createDialogueByliu(body.messages);
// 设置响应头以启用流式传输
res.setHeader('Content-Type', 'text/plain');
// 将流传递给响应对象
dataStream.pipe(res);
} catch (error) {
console.error('Stream error:', error);
res.status(500).send('Internal Server Error');
}
}
server层
@Injectable()
export class AimodelService {
private ai: ZhipuAI;
constructor() {
const apiKey = 'xxxxx';
this.ai = new ZhipuAI({ apiKey: apiKey });
}
createDialogueByliu(messages: MessageOptions[]): Promise<Readable> {
return this.ai.createCompletions({
model: 'glm-4-flash',
messages: messages,
stream: true,
}) as Promise<Readable>;
}
}
流式数据返回形式
然后前端使用antd的pro-chat作为界面,基本操作流程官方文档里有,主要是处理流式数据,如下:
data: {"id":"20241120155401f95faf63f50f42b1","created":1732089241,"model":"glm-4-flash","choices":[{"index":0,"delta":{"role":"assistant","content":"。\n "}}]}
data: {"id":"20241120155401f95faf63f50f42b1","created":1732089241,"model":"glm-4-flash","choices":[{"index":0,"delta":{"role":"assistant","content":" -"}}]}
data: {"id":"20241120155401f95faf63f50f42b1","created":1732089241,"model":"glm-4-flash","choices":[{"index":0,"delta":{"role":"assistant","content":" **"}}]}
这些数据有以下特征:
- 类型为字符串,而不是json
- 一次可能返回一组数据,也有可能是多组数据
- 我们所需要的部分是
"role":"assistant","content":" **",其中渲染内容用的是content中的内容
所以我们的思路就很明了了,使用正则匹配字符串,抽出我们所需的内容,然后将其存入字符串数组,再将字符串数组合成一个字符串,最后放入所需要渲染的位置
代码
基本形式
<div style={{ background: theme.colorBgLayout }}>
{cachedChats ? (
<ProChat
initialChats={cachedChats}
style={{
height: "92vh",
width: "100vw",
}}
helloMessage={
"欢迎使用 ProChat ,我是你的专属机器人,这是我们的 Github:[ProChat](https://github.com/ant-design/pro-chat)"
}
request={async (messages) => {。。。。
}}
/>
) : (
<></>
)}
</div>
详细代码:
request={async (messages) => {
console.log(messages);
const response = fetch(apiEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: messages,
}),
}).then((response) => {
// 确保响应是流式的
if (!response.body) {
throw new Error(
"ReadableStream not yet supported in this browser."
);
}
// 创建一个转换流来处理原始的流数据
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
const encoder = new TextEncoder();
// 创建一个新的可读流来处理数据
const transformedStream = new ReadableStream({
async start(controller) {
function push() {
reader
.read()
.then(({ done, value }) => {
if (done) {
// 当没有更多数据时关闭流
controller.close();
return;
}
//核心代码区域---------------------------------------------------------------
// 解码从流中读取的数据块
const chunk = decoder.decode(value, { stream: true });
const regex = /"role":"assistant","content":"(.*?)"/g;
//设置匹配形式
let match;
const buffer = [];
//设置缓冲流,用来存储流式读到的字符串
while ((match = regex.exec(chunk)) !== null) {
buffer.push(match[1].replace(/\\n/g, "\n"));
//将匹配到的content中的内容存入缓存中,
//同时因为返回的字符串有可能为'\n',但实质上是'\\n',
//其中一个'\'为转译字符,所以要将其替换为换行符'\n'才能被解析
}
const bufferString = buffer.join('')
//将字符串数组转换为字符串
try {
// 将处理后的数据放入流中
controller.enqueue(
encoder.encode(bufferString)
);
} catch (error) {
console.error("解析数据时发生错误", error);
controller.error(error);
}
push();
})
//核心代码区域---------------------------------------------------------------
.catch((err) => {
console.error("读取流中的数据时发生错误", err);
controller.error(err);
});
}
push();
},
});
// 返回一个新的 Response 对象,它使用我们创建的可读流
return new Response(transformedStream);
});
return response;
}}
至此就完成了流式输出