使用Nest.js+LangChain给低代码平台赋上AI代码生成能力,让低代码变成低低代码!...

 
 

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群

原文链接: https://juejin.cn/post/7408533003708940288

作者: WaiterXiao_YY 低代码Marsview核心开发

前言

LangChain 是一个用于开发由大型语言模型(LLM)支持的应用程序的框架。可以快速使用它集成各个模型,以及格式化用户输入和模型输出,封装了很多工具类,使得开发者很容易将其集成到自己的程序当中。

最近,我参与了 @河畔一角[1] 大佬的MarsView低代码平台,大家有兴趣可以去看看:www.marsview.cc/[2]

我目前提交了十几个PR了,在开发贡献过程中,我越发感觉低代码和AI应该要结合起来,让低代码更低,更加摆脱代码书写,这才是低代码平台的灵魂所在

演示

63ba6c8a1bc6d8ed3ef8cfeb3c38df77.png

20975376-3ce9-4f97-8bd1-43e4bd5a6b40.gif

上面演示了生成代码的过程。

要实现这样一个功能需要几个步骤:

  • 在前端写一个交互窗口,用于接收用户输入和展示反馈

  • 需要利用LangChain结合大模型提供后端接口,输入是用户提交的信息,输出是模型的思考

  • 前端接收接口返回值,使用打印流的形式将代码输出到编辑器中

2a624b99c23aa4ac7323ebf409c1e3c5.jpeg
Snipaste_2024-08-30_16-31-26.png

下面我将详细介绍每个步骤的实现方法

前端交互窗口和逻辑

前端是以React为技术栈的,要实现这样的窗口,可以使用一些组件库定义的模态框或者卡片组件来实现。

但是这里的交互功能比较单一,并且需要实现类似聊天的效果,所以我还是选择自己重新写一个组件。

这里创建一个窗口组件 AIChatModal.jsx.

首先是要明确这个交互窗口的功能,由于这个AI助手只是一个代码生成工具,不是一个聊天工具,所以它不需要复杂的信息流设计,能够接收一个输入,然后展示这个输入,并且反馈用户当前的状态就行。

状态有四种:

  • 执行状态:执行状态包括思考状态和代码写入状态。

    • 思考状态:向后端接口发送数据,后端接收到信息后向模型提问,模型开始思考并返回数据。

    • 代码写入状态:前端接收到接口返回后,开始写入代码,此时是写入状态

  • 完成状态:代码写入完成后显示完成状态,等待下一次输入。

  • 提示状态:当代码返回后,如果用户暂时不打算写入,可暂时不写入,不更换提示词的情况下可以再次复用代码

  • 失败状态:包括请求失败,写入失败,中断写入等。

明确状态后就可以写逻辑了,这里放置部分的代码,如果要看详细的代码,查看文末。

在窗口中写好发送信息模板和接收信息模板,以及几种状态的展示控制:

type StatusType = 'success' | 'info' | 'warning' | 'error';

const [message, setMessage] = useState<string>('');
const [requestMessage, setRequestMessage] = useState<string>('');
const [responseMessage, setResponseMessage] = useState<string>('');

const [showLoad, setShowLoad] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [loadingText, setLoadingText] = useState<string>('');
const [welcomeText, setWelcomeText] = useState<string>('MarsAI为您服务~');

const [status, setStatus] = useState<boolean>(false);
const [statusInfo, setStatusInfo] = useState<{ type: StatusType; msg: string; reload: boolean }>({
type: 'success',
msg: '生成完成',
reload: false,
});
  
<div className={styles.modalBody}>
  <div className={styles.chatContent}>
    {!requestMessage && !responseMessage && <div className={styles.welcomeText}>{welcomeText}</div>}
    {requestMessage && (
      <div className={styles.chatItem}>
        <div className={styles.chatInfo}>
          <div className={styles.chatName}>MarsUser</div>
          <div className={styles.avatar}>
            <img src="/imgs/chatUser.png" alt="" />
          </div>
        </div>
        <div className={styles.chatText}>
          <div className={styles.chatMessage}>{requestMessage}</div>
        </div>
      </div>
    )}

    {responseMessage && (
      <div className={styles.chatResponce}>
        <div className={styles.chatInfo}>
          <div className={styles.avatar}>
            <img src="/imgs/ailogo.svg" alt="" />
          </div>
          <div className={styles.chatName}>MarsAI</div>
        </div>
        <div className={styles.chatText}>
          <div className={styles.chatMessage}>{responseMessage}</div>
        </div>
      </div>
    )}

    {showLoad && (
      <div className={styles.chatLoad}>
        <div className={styles.chatText}>{loadingText}</div>
        {loading && (
          <div className={styles.load}>
            <Spin size="small" />
          </div>
        )}
      </div>
    )}

    {status && (
      <div className={styles.chatLoad}>
        {!statusInfo.reload ? (
          <Alert type={statusInfo.type} showIcon message={statusInfo.msg} />
        ) : (
          <Alert
            type={statusInfo.type}
            showIcon
            message={statusInfo.msg}
            action={
              <Popover placement="top" content="重新写入代码">
                <Button
                  style={{ marginLeft: '5px' }}
                  type="primary"
                  size="small"
                  shape="circle"
                  icon={<ReloadOutlined />}
                  onClick={onReloadWrite}
                />
              </Popover>
            }
          />
        )}
      </div>
    )}
  </div>

要实现状态的改变,只需要在窗口组件中暴露出去改变状态的方法即可:

const reloadStatus = async () => {
    setStatus(false);
    setShowLoad(true);
    setLoading(true);
    setLoadingText('正在重新写入代码,请稍后');
};

const cancelLoad = () => {
    setShowLoad(false);
    setLoading(false);
    setStatus(true);
    setStatusInfo({ type: 'info', msg: '取消写入', reload: true });
};

const writeCompleted = async () => {
    setShowLoad(false);
    setLoading(false);
    setStatus(true);
    setStatusInfo({ type: 'success', msg: '生成完成', reload: false });
};

const writeError = async () => {
    setShowLoad(false);
    setLoading(false);
    setStatus(true);
    setStatusInfo({ type: 'error', msg: '代码写入出错了', reload: false });
};

const requestError = async () => {
    setShowLoad(false);
    setLoading(false);
    setStatus(true);
    setStatusInfo({ type: 'error', msg: '请求出错了', reload: false });
};

const changeLoadInfo = async (text: string) => {
    setLoadingText(text);
    setShowLoad(true);
    setLoading(true);
};

useImperativeHandle(mRef, () => {
    return {
      cancelLoad,
      reloadStatus,
      writeCompleted,
      writeError,
      changeLoadInfo,
      requestError,
    };
});

这里使用的useImperativeHandle方法,它允许组件向外界暴露控制方法,比如打开模态框,关闭模态框,或者其他对模态框内部进行操作的方法,更好地实现组件的封装和抽离。

当然我还内置了一些经过测试有效的例子提供用户验证,还有用户输入的Input模块没有介绍,这些比较常规,去看代码就能看懂。

后端接口实现

后端是使用Nest实现的,这对前端的同学极为友好。

当然这一接口实现并不要求是Nest框架才行,实际上Mars目前的后端是koa实现的,这里主要是以Nest举例,因为Nest很好的集成了TypeScript

看上面的功能流程图就知道,这个接口应该要接收一个信息输入,并且返回模型的输出。

在自定义组件的应用场景中,我们要开发一款自定义组件,主要是修改两个文件,一个是index.jsx,另一个是config.js

  • index.jsx 用于书写组件的结构

  • config.js 用于自定义组件的属性以及属性值编辑操作,在此向组件使用用户暴露调整组件的接口

所以我们希望给模型提供一条信息,它能够帮我们写出这两个文件的内容。

在接口中,我们还应该提供多模型的选择,不管是提供给用户选择还是后端自己选择,我们都要提前做好设计,模型应该是允许切换的。

创建一个Nest项目

为了让大家复现这一功能,我将完整描述一个demo例子,跟随下面介绍,应该能够简单跑起来一个接口。

注意:Node的版本必须在18以上,因为langchain只支持18以上的版本

  • 使用 Nest-Cli 初始化项目

npm i -g yarn // 安装过yarn请忽略
npm i -g @nestjs/cli
nest new mars-ai-nest
  • 安装依赖

yarn add zod langchain @langchain/community jsonwebtoken
  • 初始化 AI Module

nest g module ai server
  • 创建 Controller

nest g controller ai server
  • 创建 Service

nest g service ai server
  • 启动服务

cd mars-ai-nest
yarn start:dev

项目启动后,就可以在各个文件中写代码了。

项目结构

我们首先来明确一下文件结构:

26f9eecc706b364fed44be448e3019bb.jpeg
Snipaste_2024-08-30_11-36-00.png

Nest中,新创建一个server,起名为ai,它将负责处理接口请求,返回数据。

provider文件夹是核心代码(自己创建):

  • base中的BaseModelProvider.ts:是对LangChain工作流的封装,它包括创建一个模型,构建prompt,新建session,获取历史记忆,模型请求,接收响应流等一系列的抽象定义。

  • model目录用于各类模型的定义,这里以glm为例,除了glm外,所有langchain支持的模型都可以使用,具体查看 支持模型[3]

  • utils是工具类,对模型的输出做解析等操作

controllerservice是大家很熟悉的mvc结构。

LangChain工作流

首先来看BaseModelProvider.ts,封装一个抽象类,定义LangChain工作流

import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { BaseMessage, MessageContent, type AIMessageChunk } from '@langchain/core/messages';
import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { Runnable, RunnableWithMessageHistory, type RunnableConfig } from "@langchain/core/runnables";
import { z } from "zod";

// 定义一个类型别名,表示一个值可能是 Promise 或者直接的值
type MaybePromise<T> = T | Promise<T>;

// 定义创建 Runnable 的选项,包括是否使用历史记录、历史记录消息和信号
export interface BaseModelProviderCreateRunnableOptions {
    useHistory?: boolean;
    historyMessages?: BaseMessage[];
    signal?: AbortSignal;
}

// 定义创建结构化输出 Runnable 的选项,包括是否使用历史记录、历史记录消息和 Zod schema
export interface BaseModelProviderCreateStructuredOutputRunnableOptions<
    ZSchema extends z.ZodType<any> = z.ZodType<any> > {
    useHistory?: boolean;
    historyMessages?: BaseMessage[];
    zodSchema?: ZSchema;
}

// 定义一个抽象类,用于提供基础模型
export abstract class BaseModelProvider<Model extends BaseChatModel> {
    // 定义一个静态属性,用于存储会话 ID 和历史记录的映射
    static sessionIdHistoriesMap : Record<string, InMemoryChatMessageHistory> = {};

    // 定义一个静态方法,用于将答案内容转换为文本
    static answerContentToText(content: MessageContent): string {
        // 如果内容是字符串,直接返回
        if(typeof content === "string") {
            return content;
        }

        // 如果内容是数组,则遍历数组,将每个元素的文本拼接起来
        return content.map(c => {
            // 如果元素类型是文本,则返回文本内容
            if(c.type === "text") {
                return c.text;
            }
            // 否则返回空字符串
            return ''
        }).join(''); 
    }

    // 定义一个可选的模型属性
    model?: Model;

    // 定义一个抽象方法,用于创建模型
    abstract createModel(): MaybePromise<Model>;   
    
    // 定义一个异步方法,用于获取模型
    async getModel(): Promise<Model> {
        // 如果模型不存在,则创建模型
        if(!this.model) {
            this.model = await this.createModel();
        }
        // 返回模型
        return this.model;
    }

    // 定义一个方法,用于创建提示
    createPrompt(options?: {useHistory?: boolean}): MaybePromise<ChatPromptTemplate> {
        // 获取选项,默认使用历史记录
        const { useHistory = true} = options ?? {};
        // 创建提示模板,包含历史记录占位符和人类消息模板
        const prompt = ChatPromptTemplate.fromMessages(
            [useHistory ? new MessagesPlaceholder('history') : '', HumanMessagePromptTemplate.fromTemplate("{input}")].filter(Boolean)
        )
        // 返回提示模板
        return prompt;
    }

    // 定义一个异步方法,用于获取历史记录
    async getHistory(sessionId: string, appendHistoryMessages?: BaseMessage[]): Promise<InMemoryChatMessageHistory> {
        // 如果会话 ID 的历史记录不存在,则创建新的历史记录
        if(BaseModelProvider.sessionIdHistoriesMap[sessionId] === undefined) {
            const messageHistory = new InMemoryChatMessageHistory();

            // 如果存在追加的历史记录消息,则添加到历史记录中
            if( appendHistoryMessages && appendHistoryMessages.length > 0) {
                await messageHistory.addMessages(appendHistoryMessages);
            }

            // 将会话 ID 和历史记录映射保存到静态属性中
            BaseModelProvider.sessionIdHistoriesMap[sessionId] = messageHistory;
        }
        // 返回会话 ID 对应的历史记录
        return BaseModelProvider.sessionIdHistoriesMap[sessionId];
    }

    // 定义一个方法,用于创建带历史记录的 Runnable
    createRunnableWithMessageHistory<Chunk extends AIMessageChunk>(
        chain:Runnable<any, Chunk, RunnableConfig>,
        historyMessages: BaseMessage[]
    ) {
        // 创建一个带历史记录的 Runnable,并设置相关参数
        return new RunnableWithMessageHistory(
           {
            runnable: chain,
            // 获取历史记录的回调函数
            getMessageHistory: async sessionId => await this.getHistory(sessionId, historyMessages),
            // 输入消息的键
            inputMessagesKey: 'input',
            // 历史消息的键
            historyMessagesKey: 'history'
           }
        );
    }

    // 定义一个异步方法,用于创建 Runnable
    async createRunnable(options?: BaseModelProviderCreateRunnableOptions) {
        // 获取选项,默认使用历史记录
        const { useHistory = true, historyMessages = [], signal } = options ?? {};
        // 获取模型
        const model = await this.getModel();
        // 创建提示
        const prompt = await this.createPrompt({useHistory});
        // 创建链,将提示和模型连接起来
        const chain = prompt.pipe(signal ? model.bind({signal}): model);
        // 如果使用历史记录,则创建带历史记录的 Runnable
        return useHistory ? await this.createRunnableWithMessageHistory(chain, historyMessages || []) : chain;
    }

    // 定义一个异步方法,用于创建结构化输出 Runnable
    async createStructuredOutputRunnable<ZSchema extends z.ZodType<any>>(
        options?: BaseModelProviderCreateStructuredOutputRunnableOptions<ZSchema>
    ) {
        // 获取选项,默认使用历史记录
        const { useHistory = true, historyMessages = [], zodSchema } = options ?? {};
        // 获取模型
        const model = await this.getModel();
        // 创建提示
        const prompt = await this.createPrompt({useHistory});
        // 创建链,将提示和模型连接起来
        const chain = prompt.pipe(model);
        // 如果使用历史记录,则创建带历史记录的 Runnable
        return useHistory ? await this.createRunnableWithMessageHistory(chain, historyMessages || []) : chain;
    }

}

我给出了详细的注释,所以不多以描述。

定义模型

刚刚说到,基本上市面上的大模型LangChain都支持,但是由于科学原因,国外的模型并不能很愉快的使用,这里我也经过测试,智谱的GLM是一个很不错的模型,推荐大家使用,这里以智谱的GLM模型为例子,如果大家需要更多模型,参考LangChain官方文档的描述相应配置即可。

import { BaseModelProvider } from '../base/BaseModelProvider';

// 导入智谱AI的聊天模型
import { ChatZhipuAI } from '@langchain/community/chat_models/zhipuai';

// 定义一个 GlmModelProvider 类,它继承自 BaseModelProvider,并指定模型类型为 ChatZhipuAI
export class GlmModelProvider extends BaseModelProvider<ChatZhipuAI> {
  // 异步创建模型方法
  async createModel() {
    // 设置智谱AI的API密钥
    const aiKey = '你的密钥';
    // 设置模型名称
    const model_name = 'glm-4';

    // 使用 ChatZhipuAI 类创建模型实例
    const model = new ChatZhipuAI({
      // 设置 API 密钥
      apiKey: aiKey,
      // 设置模型名称
      model: model_name,
      // 设置温度参数,用于控制模型的随机性,取值范围为 0 到 1,建议不要使用 1.0,因为有些模型不支持
      temperature: 0.95, 
      // 设置最大重试次数,如果请求失败,会尝试重试最多 3 次
      maxRetries: 3,
      // 设置是否显示详细日志,true 表示显示
      verbose: true
    });

    // 返回创建的模型实例
    return model;
  }
}

很简单,只需要更改你的密钥就行,密钥的获取去智谱开放平台[4]获取。

Controller

在将LangChain工作流都定义好后,接下来就实现接口的细节,首先在 ai.controller.ts 写好请求入口:

// 控制器类,用于处理与 AI 相关的请求
@Controller('ai')
export class AiController {
  // 构造函数,注入 AI 服务
  constructor(private readonly aiService : AiService) {}

  // 处理 POST 请求,用于代码生成
  // 路径为 /ai/lib/chat
  // 接收请求体中的 message 参数,类型为字符串
  // 返回一个 Promise 对象,解析后为一个对象
  @Post("/lib/chat")
  async codeGenerate(@Body('message') message : string): Promise<object> {
    // 调用 AI 服务的 codeGenerate 方法,传入 message 参数
    // 返回结果存储在 result 变量中
    const result = await this.aiService.codeGenerate(message);
    // 返回一个包含代码执行结果的对象
    // code 字段表示状态码,0 表示成功
    // msg 字段表示消息,success 表示成功
    // data 字段包含生成代码和配置信息
    return {
      code: 0,
      msg: "success",
      data: {
        // jsx 字段存储生成的 JSX 代码
        jsx: result[0],
        // config 字段存储生成的配置信息
        config: result[1]
      }
    }
  }
}

Service

service也是比较重要的处理逻辑所在,在这里需要构建prompt,提交模型,接收响应流,处理响应流。

LangChain如何实现一个响应呢?

const buildStream = async () => {
  // 声明一个名为 aiStream 的变量,用于存储 AI 流,初始值为 null
  let aiStream = null;
  // 检查会话历史记录是否存在,如果不存在
  if (!isSessionHistoryExists) {
    // 从 sessionIdHistoriesMap 中删除与当前 sessionId 对应的历史记录
    delete sessionIdHistoriesMap[sessionId];
    // 使用 aiRunnable.stream 方法创建 AI 流,并传递 prompt 作为输入参数和 aiRunnableConfig 作为配置参数
    aiStream = aiRunnable.stream(
      {
        input: prompt
      },
      aiRunnableConfig
    );
  } else {
    // 如果会话历史记录存在,则使用 aiRunnable.stream 方法创建 AI 流,并传递一个包含继续提示的字符串作为输入参数,以及 aiRunnableConfig 作为配置参数
    aiStream = aiRunnable.stream(
      {
        input: `
                          continue, please do not reply with any text other than the code, and do not use markdown syntax.
                          go continue.
                      `
      },
      aiRunnableConfig
    );
  }
  // 返回创建的 AI 流
  return aiStream;
};

这里我是保留了会话和历史记录的,当然,这一操作在此其实并没有什么作用,因为在这里不支持对话,所以也就不需要上下文的记忆,但是为了工作流的完整性,我还是保留了。

主要是使用了aiRunnable这一工具类,它来自@langchain/core/runnables,是LangChain处理响应流的核心包,可以调用、批处理、流式传输、转换和组合。文档[5]

在得到响应流的输出后,需要对响应流进行处理,在模型的输出中,它包含了index.jsxconfig.js两个文件的代码,所以我们需要对其进行解析,将代码得到

先要实现一个解析方法,采用正则解析的方式:

export const extractCodeBlocks = (str: string): string[] => {
  // 如果字符串为空,则返回空数组
  if (!str) {
    return [];
  }

  // 正则表达式匹配所有的代码块内容
  // ``` 开头,``` 结尾,中间包含任意字符,包括换行符
  // 捕获组 $1 匹配代码块内容
  const matches = str.match(/```[\s\S]*?\n([\s\S]*?)\n```/g);

  // 如果找到了匹配项,则返回提取的内容,否则返回空数组
  // 使用 map 函数对匹配到的结果进行处理,提取代码块内容并去除空格
  return matches ? matches.map(match => match.replace(/```[\s\S]*?\n([\s\S]*?)\n```/, '$1').trim()) : [];
};

然后对响应流进行解析和拼接:

let result = [];
const aiStream = await buildStream();
console.log(aiStream);
if(aiStream) {
    for await (const chunk of aiStream) {
        const text = GlmModelProvider.answerContentToText(chunk.content);
        result.push(text);
    }
}
const ai_stream_string = result.join('');
const code_array = extractCodeBlocks(ai_stream_string);

实际上,LangChain是支持流式输出的,aiStream里面有很多个chunck,每个chunck都是一次流式返回,但是这里我是等所有的输出结束后再统一进行处理,这里并不是主流的方法,后续版本我会对此进行改进。

构建Prompt

工作流程都搭建完了,用户信息能够接收到了,也能向模型提问了,那么如何保证模型的输出符合我们的要求呢。

大家都知道模型会有一个幻觉的问题,也就是答非所问,似有道理,但实际不行,为了解决这个问题,可以采取微调或者RAG的方法实现。

在这里,我们实现了一个类似RAG的方法,RAG是检索增强,会有一个知识库,但是目前我们还没有搭建起来这个知识库,一旦搭建好这个知识库,我们可以通过向量检索的方式实现模型的prompt,那么生成能力和准确能力将得到大大提升。

这里暂时只是构建了一个比较好的prompt,用于调整模型的角色以及提供一些必要的提示。

async buildGeneratePrompt({message}: {message: string}) {
    const codePrompt = `
    You are a low-code component development expert.
    Your task is to help me generate two files for a low-code development module: index.jsx and config.js.
    The index.jsx file should define the structure of the component using React and Ant Design, and the config.js file should specify the component's property configurations.

    Here’s an example of a login form component:

    index.jsx file:
    // jsx
    export default ({ id, type, config, onClick }, ref) => {
        const { Form, Button, Input } = window.antd;
        const onFinish = (values) => {
          onClick && onClick(values);
        }
        return (
          <div data-id={id} data-type={type}>
            <Form name="login"
              labelCol={{ span: config.props.labelCol }}
              wrapperCol={{ span: config.props.wrapperCol }}
              style={{ maxWidth: config.props.maxWidth }}
              onFinish={onFinish}
            >
              <Form.Item label="用户名" name="username">
                <Input />
              </Form.Item>
              <Form.Item label="密码" name="password">
                <Input.Password />
              </Form.Item>
              <Form.Item wrapperCol={{
                offset: config.props.offset,
                span: config.props.wrapperCol
              }}>
                <Button htmlType="submit" block={config.props.block} type="primary">
                  {config.props.loginBtn}
                </Button>
              </Form.Item>
            </Form>
          </div>
        );
      };

      config.js file:
      // config.js
        export default {
            attrs: [
                {
                type: 'Title',
                label: '基础设置',
                key: 'basic'
                },
                {
                type: 'Input',
                label: '登陆名称',
                name: ['loginBtn']
                },
                {
                type: 'Switch',
                label: '块状按钮',
                name: ['block']
                },
                {
                type: 'InputNumber',
                label: 'LabelCol',
                name: ['labelCol']
                },
                {
                type: 'InputNumber',
                label: 'WrapperCol',
                name: ['wrapperCol']
                },
                {
                type: 'InputNumber',
                label: 'Offset',
                name: ['offset']
                },
                {
                type: 'InputNumber',
                label: 'MaxWidth',
                name: ['maxWidth']
                }
            ],
            config: {
                props: {
                loginBtn: '登陆',
                block: true,
                labelCol: 8,
                wrapperCol: 16,
                offset: 8,
                maxWidth: 700
                },
                style: {},
                events: [],
            },
            events: [
                {
                value: 'onClick',
                name: '登陆事件'
                }
            ],
            methods: [],
        };
        Note: If you need to import hooks like useState and useEffect, you should import them using const { useState, useEffect } = window.React;. 
        Similarly, Ant Design components should be imported in this way: const { Button, Form, DatePicker, Tag } = window.antd;.
        Now, based on the above structure and configuration, I need you to generate the index.jsx and config.js files for a new component. 
        The description of this component in Chinese is #{message}. You can understand it in English and help me implement the code of the component
        Please return only the code for both files, without any additional descirption text or markdown syntax.
    `

    const prompt = codePrompt.replace("#{message}", message);
    return prompt;
}

到此,整个接口的实现细节就讲完了,这里放一下完整的service代码。

import { Injectable } from '@nestjs/common';
import { GlmModelProvider } from './provider/model/glm';
import { RunnableConfig } from '@langchain/core/runnables';
import { extractCodeBlocks } from './provider/utils';

@Injectable()
export class AiService {
    
    async buildGeneratePrompt({message}: {message: string}) {
        // 复制上面的
    }


    async codeGenerate(message: string): Promise<Array<string>> {
        const modelProvider = new GlmModelProvider();

        const aiRunnableAbortController = new AbortController();
        const aiRunnable = await modelProvider.createRunnable({
          signal: aiRunnableAbortController.signal
        });
        
        const sessionId = `code_session_${Date.now()}`;

        const aiRunnableConfig: RunnableConfig = {
            configurable: {
                sessionId
            }
        }

        const sessionIdHistoriesMap = await GlmModelProvider.sessionIdHistoriesMap;
        
        const isSessionHistoryExists = !!sessionIdHistoriesMap[sessionId];
      
        const prompt = await this.buildGeneratePrompt({message});

        const buildStream = async () => {
          let aiStream = null;
          if(!isSessionHistoryExists) {
              delete sessionIdHistoriesMap[sessionId];
              aiStream = aiRunnable.stream(
                  {
                      input: prompt
                  },
                  aiRunnableConfig
              )
          } else {
              aiStream = aiRunnable.stream(
                  {
                      input: `
                          continue, please do not reply with any text other than the code, and do not use markdown syntax.
                          go continue.
                      `
                  },
                  aiRunnableConfig
                );
          }
          return aiStream;
        }
        
        let result = [];
        const aiStream = await buildStream();
        if(aiStream) {
            for await (const chunk of aiStream) {
                const text = GlmModelProvider.answerContentToText(chunk.content);
                result.push(text);
            }
        }
        const ai_stream_string = result.join('');
        const code_array = extractCodeBlocks(ai_stream_string);

        return [code_array[0], code_array[1]];
    }
}

Postman 调试

重新启动Nest项目,测试一下接口

a34b45657c12793ebbae53e705b0f6fb.jpeg
Snipaste_2024-08-30_14-19-32.png

前端写入代码

接口接收到返回值中有index.jsxconfig.js两个文件的代码。

此时不能盲目的向编辑器写入代码,因为编辑器中可能存有代码,覆写将导致代码丢失,所以要做好提示

ef016ffb019b6a5e8bb67fbc58037fe5.jpeg
Snipaste_2024-08-30_13-42-31.png

当用户取消的时候,缓存返回的代码,避免再次请求接口,但是反馈用户提示状态,用户可以再次选择写入代码:

798c00c09b3580d3f40ad337b2b9f7af.jpeg
Snipaste_2024-08-30_13-43-14.png

而当用户确认写入的时候,使用定时器将代码写入到编辑器中:

async writeCode(newCode: string): Promise<boolean> {
    return new Promise((resolve) => {
      let index = 0;
      const codeInterval = setInterval(() => {
        setCode((prev) => prev + newCode[index++]);
        if (index > newCode.length - 2) {
          clearInterval(codeInterval);
          setRefreshTag(refreshTag + 1);
          resolve(true);
        }
      }, 30);
    });
},

具体的前端代码,看文末说明。

至此,这一功能就全部完成了,这里主要是使用了LangChain这个库跟大模型打交道,实际上它也只是对官方提供的api进行封装,如果不需要使用这个库,直接使用官方的api也是可以的。

最后

MarsView低代码平台是开源的,可以去github上查看前端的代码,后端因为正在重构中,暂不开源,但是上面我所描述的,跑起一个接口是没问题的。

欢迎在线体验:www.marsview.cc/[6]

开源地址:github.com/JackySoft/m…[7]

文档地址:docs.marsview.cc/[8]

参考资料

[1]

https://juejin.cn/user/1996368846258222: https://juejin.cn/user/1996368846258222

[2]

http://www.marsview.cc/: http://www.marsview.cc/

[3]

https://python.langchain.com/v0.2/docs/integrations/chat/: https://python.langchain.com/v0.2/docs/integrations/chat/

[4]

https://open.bigmodel.cn/: https://open.bigmodel.cn/

[5]

https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html: https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html

[6]

http://www.marsview.cc/: http://www.marsview.cc/

[7]

https://github.com/JackySoft/marsview: https://github.com/JackySoft/marsview

[8]

http://docs.marsview.cc/: http://docs.marsview.cc/

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

   “分享、点赞、在看” 支持一波👍
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值