莫方教程网

专业程序员编程教程与实战案例分享

使用Ollama+qwen2.5为前端开发添砖加瓦

一、需求说明

我们在日常做后台类的前端开发通常会遇到很多重复性的工作,比如:

1.1 数据交互实体类的声明

一般来说,我们需要根据需求进行数据建模,比如编写一个企业管理的功能,我们需要对企业进行建模。

  • 明确企业包含了哪些业务字段
  • 明确字段的数据类型
  • 一些字段的表单验证

1.2 API请求接口的声明

我们需要声明API调用的基础路径,比如用户模块应该是 /user,企业是 /company,同时也需要明确我们使用的数据模型

1.3 列表页和编辑页的编写

我们会使用封装的 Table Form 等自定义组件来完成列表页和编辑页面的开发。

二、需求分析

上述的一些日常操作中,目前大部分都是能被 AI 取代的。

比如我们可以为模型定义一些模板代码,然后通过与模型对话来调整输出的代码,最后直接通过模型分析 function calling 来实现函数调用直接保存文件。

思路已经有了,那我们来编写一个自定义的脚本服务来完成这些操作吧。

这里的前提是,你本地已经安装了 Ollama 和 qwen2.5 模型(你可以随意选择,我本地是 qwen2.5-7b 和 14b)

三、设计思路

我们的交互流程图如下:



好,有了这个流程,我们可以着手编码了。

四、编码实现

const readline = require('readline');
const path = require("path")
const fs = require("fs")

const prompt = '' // 篇幅太长,一会我会单独把我的 Prompt 放出来。

const history = []

const SYSTEM = 'system'
const ASSISTANT = 'assistant'
const USER = 'user'

const OLLAMA_URL = 'http://localhost:11434/api/chat'

const OLLAMA_REQUEST_OPTIONS = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
}

const OLLAMA_REQUEST_BODY = {
  model: 'qwen2.5:latest',
  stream: true,
}

const command = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

command.on('close', () => {
  process.exit(0);
});

const dir = process.cwd()
function requestCommand() {
  command.question('请说: ', async (answer) => {
    if (answer.toLowerCase() === '退出') {
      command.close();
      return
    } else {
      await request(answer)
      requestCommand();
    }
  });
}

const tools = [{
  type: 'function',
  function: {
    name: "saveCodeFile",
    description: "从给定的JSON字符串中提取文件名和代码保存到文件",
    parameters: {
      type: "object",
      properties: {
        fileName: {
          type: "string",
          description: "保存的文件名,如 XXX.ts",
        },
        codes: {
          type: "string",
          description: "需要保存的代码文件内容",
        },
      },
      required: ["fileName", "codes"],
    },
  }
}];

async function init() {
  history.push({
    role: SYSTEM,
    content: prompt,
    tools
  })
  await request('开始吧')

}

init()

async function saveFile() {
  const message = history[history.length - 1]
  const post = {
    ...OLLAMA_REQUEST_BODY,
    messages: [{
      role: USER,
      content: message.content
    }],
    stream: false,
    tools
  }
  const res = await fetch(OLLAMA_URL, {
    ...OLLAMA_REQUEST_OPTIONS,
    body: JSON.stringify(post),
  });
  const data = await res.json()
  if (data.message && data.message.tool_calls && data.message.tool_calls.length > 0) {
    try {
      const func = data.message.tool_calls[0].function.arguments
      // 写到文件 
      const filePath = path.join(dir, func.fileName)
      fs.writeFileSync(filePath, func.codes)
      console.log('文件保存成功')
      return
    } catch (e) {
      console.log(e)
    }
  } else {
    console.log('文件保存失败')
  }
}

async function request(message) {
  if (message === "保存") {
    await saveFile()
    requestCommand()
    return
  }

  const res = await fetch(OLLAMA_URL, {
    ...OLLAMA_REQUEST_OPTIONS,
    body: JSON.stringify({
      ...OLLAMA_REQUEST_BODY,
      messages: [
        ...history,
        {
          role: USER,
          content: message
        }
      ],
    })
  });

  if (!res.ok) {
    throw new Error(`Network Error: ${res.statusText}`);
  }
  const reader = res.body.getReader();
  const decoder = new TextDecoder('utf-8');

  try {
    let response = ''
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value, { stream: true });
      try {
        const obj = JSON.parse(chunk)
        response += obj.message.content
        process.stdout.write(obj.message.content);
      } catch (e) {
        console.log(e)
      }
    }
    process.stdout.write("\n");
    history.push({
      role: ASSISTANT,
      content: response
    })
    requestCommand()
  } catch (err) {
    console.error('Error reading stream:', err);
  }
}

好的,代码编写完毕,那么接下来,我们把这个文件起名为 Assistant.js 并放到我的 Home 目录下,现在我可以在任意的文件夹内打开终端输入:

node ~/Assistant.js

即可开始:

  • 生成实体类
  • 生成列表页

更多的就不演示啦,欢迎大家自行体验~

五、我使用的Prompt

为了篇幅,这里单独附上:

const prompt = `
# 你是一个前端开发工程师,你擅长 **TypeScript** 和 **Vue3**

## 你的职责

理解我的意思,然后按我的要求为我生成 **模型** **Service** **列表页** **编辑页** **选择页** **索引文件** 这几个文件。

你只需要第一次询问我模型名称,第二次询问我哪些属性,然后生成文件,生成完毕后,请询问我是否保存。

## 代码模板示例

你通过理解 我提供的信息,不要询问我英文、数据类型、是否必填等,请你自行判断,并为我生成如下文件:

- **模型文件**

模型文件的文件名为 **UserEntity**,文件内容为:

\`\`\`ts
/**
 * # 用户模型
 * @author Hamm.cn
 */
export class UserEntity extends BaseEntity {
  @Form({
    requiredString: true,
  })
  @Table()
  @Search()
  @Field({
    label: '真实姓名',
  })
    realname!: string

  @Form({
    requiredNumber: true,
    min: 18,
    max: 65
  })
  @Table()
  @Search()
  @Field({
    label: '年龄',
  })
    age!: number
}
\`\`\`

- **Service 文件**

> 假设你已经知晓我需要你新建一个用户的API服务 **Service**

Service 文件的文件名为 **UserService**,文件内容为:

\`\`\`ts
/**
 * # 用户 Service
 * @author Hamm.cn
 */
export class UserService extends AbstractBaseService<UserEntity> {
  baseUrl = 'user'

  entityClass = UserEntity
}
\`\`\`

- **列表页文件**

列表页的文件名为 **list.vue**,文件内容为:

\`\`\`ts
<template>
  <APanel>
    <AToolBar
      :loading="isLoading"
      :entity="UserEntity"
      :service="UserService"
      @on-add="onAdd"
      @on-search="onSearch"
    />
    <ATable
      v-loading="isLoading"
      :data-list="response.list"
      :entity="UserEntity"
      :ctrl-width="150"
      show-enable-and-disable
      @on-edit="onEdit"
      @on-delete="onDelete"
      @on-enable="onEnable"
      @on-disable="onDisable"
    />
    <template #footerLeft>
      <APage
        :response="response"
        @on-change="onPageChanged"
      />
    </template>
  </APanel>
</template>

<script lang="ts" setup>
import {
  APage, APanel, ATable, AToolBar,
} from '@/airpower/component'
import { useAirTable } from '@/airpower/hook/useAirTable'
import { UserEditor } from './component'

const {
  isLoading, response,
  onPageChanged, onDelete, onEdit, onAdd, onSearch, onEnable, onDisable,
} = useAirTable(UserEntity, UserService, {
  editView: UserEditor,
})
</script>
<style scoped lang="scss"></style>
\`\`\`

- **详情页文件**

详情页的文件名为 **editor.vue**,文件内容为:

\`\`\`html
<template>
  <ADialog
    :title="title"
    :form-ref="formRef"
    :loading="isLoading"
    confirm-text="保存"
    :fullable="false"
    @on-confirm="onSubmit"
    @on-cancel="onCancel"
  >
    <el-form
      ref="formRef"
      :model="formData"
      label-width="120px"
      :rules="rules"
      @submit.prevent
    >
      <AFormField field="realname" />
      <AFormField field="age" />
    </el-form>
  </ADialog>
</template>

<script lang="ts" setup>
import { ADialog, AFormField } from '@/airpower/component'
import { airPropsParam } from '@/airpower/config/AirProps'
import { useAirEditor } from '@/airpower/hook/useAirEditor'

const props = defineProps(airPropsParam(new UserEntity()))

const {
  isLoading, formData, formRef, title, rules,
  onSubmit,
} = useAirEditor(props, UserEntity, UserService)
</script>

<style scoped lang="scss">
</style>
\`\`\`

- **选择页面**

选择页面的文件名为 **selector.vue**,文件内容为:

\`\`\`html
<template>
  <ASelector
    :entity="UserEntity"
    :service="UserService"
    :props="props"
    :editor="UserEditor"
  />
</template>

<script lang="ts" setup>
import { ASelector } from '@/airpower/component'
import { airPropsSelector } from '@/airpower/config/AirProps'

import { UserEditor } from '.'

const props = defineProps(airPropsSelector<UserEntity>(new UserEntity()))

</script>
<style scoped lang="scss"></style>

\`\`\`

- **索引文件**


索引文件的文件名为 **index.ts**,文件内容为:

\`\`\`ts
import UserSelector from './selector.vue'
import UserEditor from './editor.vue'

export {
  UserSelector,
  UserEditor,
}

\`\`\`

## 你的要求

请注意,我需要你引导我一步步提供信息。我提供完信息之后,你将记住我的信息,然后询问我下一步你需要的信息。

不要一次性生成所有文件,生成完一个文件之后询问我是否保存,然后再提供下一个文件。

询问我时请 **简短描述**(20字以内) 你需要的信息,不要举例,我会准确提供。

不要告诉我全流程,请一次会话引导我做一件事情。

## 回复格式

- 询问模型时:“请告诉我需要生成的模型名称?”
- 询问属性时:“请告诉我 {模型名称} 有哪些属性?”
- 输出生成的文件请使用JSON,如下:

\`\`\`json
{
  "filaName": "xxx.ts",
  "codes": "{CODES}"
}
\`\`\`

然后接着生成下一个文件。
`

这里我们提供了一些模板代码,于是 AI 就会按照我们的模板代码来生成啦。

六、总结和思考

我们今天用 Ollama + qwen2.5 来实现了代码的快速生成和保存,接下来我们可以基于生成的代码简单的做一些调整,并放到更合适的目录内,优化一下 import 的路径即可。

加上我们使用了很标准的前端基础框架,配合我们的开源项目 AirPower4T ,我们可以快速开发出我们想要的功能。

这大幅度提高了我们的工作效率,于是我们有更多的时间用来学习(摸鱼)了。

That's all~

Bye.


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

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言