百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

haoteby 2024-12-09 12:48 1 浏览

一、需求说明

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

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

相关推荐

网站seo该怎么优化

一、网站定位在建设一个网站之前,我们首先要做的就是一个网站清晰的定位,会带来转化率相对较高的客户群体,我们建站的目的就是为了营销,只有集中来做某一件事,才会更好的展现我们的网站。在做SEO优化的同时...

3个小技巧教你如何做好SEO优化

  想半路出家做SEO?可是,怎么才做的好呢?关于SEO专业技术弄懂搜索引擎原理,咱们做搜索引擎排名的首先就是要了解搜索引擎的工作原理,对SEO优化有更深入了解之后再来做SEO,你就能从搜索引擎的视点...

SEO指令分享:filetype指令

filetype用于搜索特定的文件格式。百度和谷歌都支持filetype指令。比如搜索filetype:pdf今日头条返回的就是包含今日头条这个关键词的所有pdf文件,如下图:百度只支持:pdf...

网站seo优化技巧大全

SEO在搜索引擎中对检索结果进行排序,看谁最初是在用户的第一眼中看到的。实际上,这些排名都是通过引擎的内部算法来实现的。例如,百度算法很有名。那么,对百度SEO的优化有哪些小技巧?下面小编就会说下针对...

小技巧#10 某些高级的搜索技巧

由于某些原因,我的实验场所仅限百度。1.关键词+空格严格说来这个不能算高级,但关键词之间打空格的办法确实好用。我习惯用右手大拇指外侧敲击空格键,这个习惯在打英文报告时尤其频繁。2.site:(请不要忽...

MYSQL数据库权限与安全

权限与安全数据库的权限和数据库的安全是息息相关的,不当的权限设置可能会导致各种各样的安全隐患,操作系统的某些设置也会对MySQL的安全造成影响。1、权限系统的工作原理...

WPF样式

UniformGrid容器<UniformGridColumns="3"Rows="3"><Button/>...

mysql自动备份,并zabbix检测备份文件是否正常,备份文件大小

推荐...

MySQL学到什么程度?才有可以在简历上写精通

前言如今互联网行业用的最多就是MySQL,然而对于高级Web面试者,尤其对于寻找30k下工作的求职者,很多MySQL相关知识点基本都会涉及,如果面试中,你的相关知识答的模糊和不切要点,基...

jquery的事件名称和命名空间的方法

我们先看一些代码:当然,我们也可以用bind进行事件绑定。我们看到上面的代码,我们可以在事件后面,以点号,加我们的名字,就是事件命名空间。所谓事件命名空间,就是事件类型后面以点语法附加一个别名,以便引...

c#,委托与事件,发布订阅模型,观察者模式

什么是事件?事件(Event)基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件。通过委托使用事件事件在类中声明且生成,且通过...

前端分享-原生Popover已经支持

传统网页弹窗开发需要自己处理z-index层级冲突、编写点击外部关闭的逻辑、管理多个弹窗的堆叠顺序。核心优势对比:...

Axure 8.0 综合帖——新增细节内容

一、钢笔工具与PS或者AI中的钢笔工具一样的用法。同样有手柄和锚点,如果终点和起点没有接合在一起,只要双击鼠标左键即可完成绘画。画出来的是矢量图,可以理解为新的元件。不建议通过这个工具来画ICON图等...

PostgreSQL技术内幕28:触发器实现原理

0.简介在PostgreSQL(简称PG)数据库中,触发器(Trigger)能够在特定的数据库数据变化事件(如插入、更新、删除等)或数据库事件(DDL)发生时自动执行预定义的操作。触发器的实现原理涉及...

UWP开发入门(十七)--判断设备类型及响应VirtualKey

蜀黍我做的工作跟IM软件有关,UWP同时会跑在电脑和手机上。电脑和手机的使用习惯不尽一致,通常我倾向于根据窗口尺寸来进行布局的变化,但是特定的操作习惯是依赖于设备类型,而不是屏幕尺寸的,比如聊天窗口的...