基于ollama+Agent+workFlow工作流 根据提示词操作电脑软件

目录

效果

编写agent

workFlow工作流

代码地址:


前面在文章ollama+qwen2.5vl:7b多模态做图片和文件分析-CSDN博客

中介绍了多模态大模型,通过调用ollama接口解读文件和图片。

这里我们开始做Agent,就是根据提示词操作电脑软件。

效果

先给效果,我还是用多模态大模型,做一个网页,然后给出提示词:"Edge 浏览器搜索最新热点新闻",然后就能自动打开浏览器了,并且自动搜索了相关内容。

编写agent

代码地址:见文章底部

下载后,npm install npm run build npm start 就能打开浏览器了。然后右上角有个控制电脑,AI就能操作电脑了。

关键代码:基于前面的文章学习,这里最关键的就是新增了一个server/services/agent.js

看接口入口:在server/index.js文件中:

复制代码
  // POST /api/agent/execute - 执行自然语言指令
  'POST /api/agent/execute': async (req, res) => {
    const body = await parseBody(req);
    const { command } = body;

    if (!command) {
      return json(res, 400, { error: '指令不能为空' });
    }

    try {
      const result = await agent.execute(command);
      json(res, 200, result);
    } catch (error) {
      json(res, 500, { success: false, message: `执行失败:${error.message}` });
    }
  },

然后进入到agent.js,给输入的句子加上系统提示词,这点很重要,我们要去大模型必须给我们返回固定的格式,方便我们解析固定的操作软件。

复制代码
// ========== 2. 意图识别 ==========

/**
 * 构建系统提示词,告诉 AI 如何解析指令
 */
function buildSystemPrompt() {
  const toolDescriptions = Object.entries(TOOLS).map(([name, tool]) => {
    const paramDesc = Object.entries(tool.parameters || {})
      .map(([key, desc]) => `    - ${key}: ${desc}`)
      .join('\n');
    return `- ${name}: ${tool.description}\n${paramDesc}`;
  }).join('\n\n');

  return `你是一个电脑助手,负责将用户的自然语言指令转化为结构化的操作。

## 可用操作

${toolDescriptions}

## 输出格式

你必须以 JSON 格式输出,不要输出其他任何内容:

{
  "action": "操作名称(从上面的列表中选择)",
  "parameters": {
    "参数名": "参数值"
  }
}

## 规则

1. action 必须从可用操作列表中选择
2. parameters 必须包含该操作所需的所有必填参数
3. 如果用户只是闲聊,没有操作意图,使用 action: "chat"
4. 只输出 JSON,不要有任何解释、注释或 markdown 标记
5. 如果用户说的不明确,尽量选择最合理的操作`;
}

然后调用大模型,解析返回结果中的固定action字段,和描述字段。如果解析不了,就用聊天对话兜底。

复制代码
/**
 * 解析用户指令,返回结构化操作
 * @param {string} userInput - 用户输入的自然语言
 */
async function parseIntent(userInput) {
  const schema = JSON.stringify({
    action: 'string, 操作名称',
    parameters: 'object, 参数键值对'
  }, null, 2);

  const systemPrompt = buildSystemPrompt();

  const response = await ollama.chat(userInput, [], systemPrompt);

  // 尝试从 AI 回复中提取 JSON
  let jsonStr = response.trim();
  console.log("大模型回复内容: ", jsonStr)

  // 去掉 markdown 代码块标记
  if (jsonStr.startsWith('```json')) {
    jsonStr = jsonStr.replace(/```json\n?/, '').replace(/\n?```/, '').trim();
  } else if (jsonStr.startsWith('```')) {
    jsonStr = jsonStr.replace(/```\n?/, '').replace(/\n?```/, '').trim();
  }

  // 尝试找 JSON 花括号包裹的内容
  const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
  if (jsonMatch) {
    jsonStr = jsonMatch[0];
  }

  try {
    const parsed = JSON.parse(jsonStr);

    // 验证 action 是否在白名单中
    if (!TOOLS[parsed.action]) {
      throw new Error(`未知的操作类型:${parsed.action}`);
    }

    return {
      action: parsed.action,
      parameters: parsed.parameters || {},
      raw: response
    };
  } catch (e) {
    console.error('JSON 解析失败:', e.message);
    console.error('AI 原始回复:', response);

    // 降级:当作聊天处理
    return {
      action: 'chat',
      parameters: { reply: '抱歉,我没理解您的指令,能再说清楚一点吗?' },
      raw: response,
      parseError: e.message
    };
  }
}

然后添加一个Tool枚举工具类。去匹配我们解析的action行为,比如说action=open_browser\open_app\open_file等,

比如说打开浏览器:

复制代码
  /**
   * 打开浏览器并访问指定 URL
   */
  open_browser: {
    description: '打开浏览器(Edge/Chrome)并访问指定网址或搜索内容',
    parameters: {
      url: '要访问的完整网址,如 https://www.bing.com',
      search: '搜索关键词(与 url 二选一)',
      browser: '浏览器类型:edge 或 chrome,默认 edge'
    },
    async execute(params) {
      const browser = params.browser || 'edge';
      let targetUrl = params.url;

      // 如果提供了搜索关键词,构造搜索 URL
      if (params.search && !targetUrl) {
        const encoded = encodeURIComponent(params.search);
        targetUrl = `https://www.bing.com/search?q=${encoded}`;
      }

      if (!targetUrl) {
        throw new Error('缺少 url 或 search 参数');
      }

      // Windows 命令
      // 主要是execAsync函数,挺牛的,通过命令告诉操作系统,启动浏览器。
      if (browser === 'chrome') {
        await execAsync(`start chrome "${targetUrl}"`);
      } else {
        await execAsync(`start msedge "${targetUrl}"`);
      }

      return { success: true, message: `已打开 ${browser} 浏览器,访问 ${targetUrl}` };
    }
  },

这里面有个很重要的函数execAsync 可以直接通知windows操作系统打开对应的app. 并且访问对应的地址。

这些就差不多了。

然后我们看运行的日志,这里是一整段:

复制代码
[Agent] 收到指令: 打开记事本
[Agent] 正在解析意图...
工具描述,toolDescriptions= - open_browser: 打开浏览器(Edge/Chrome)并访问指定网址或搜索内容
    - url: 要访问的完整网址,如 https://www.bing.com
    - search: 搜索关键词(与 url 二选一)
    - browser: 浏览器类型:edge 或 chrome,默认 edge

- open_app: 打开本地已安装的应用程序,如 notepad、calc、cmd 等
    - app: 应用名称或路径,如 notepad、calc、mspaint、cmd、explorer
    - args: 传递给应用的参数(可选)

- open_file: 用系统默认程序打开文件,或在资源管理器中打开文件夹
    - path: 文件或文件夹的完整路径
    - folder: 是否以文件夹模式打开(true 则在资源管理器中打开所在目录)

- run_command: 执行系统命令(仅限安全命令,如 dir、echo、ping 等)
    - command: 要执行的命令字符串
    - cwd: 工作目录(可选)

- sys_info: 获取系统信息,如 IP 地址、磁盘空间、内存使用等
    - type: 信息类型:ip、disk、memory、cpu、all,默认 all

- chat: 当用户只是聊天、询问信息,不需要操作电脑时使用
    - reply: 给用户的回复内容

-----------------------------------------------------------------------
大模型回复内容:  {
  "action": "open_app",
  "parameters": {
    "app": "notepad"
  }
}

-------------------------------------------------------------------
[Agent] 解析结果: {
  "action": "open_app",
  "parameters": {
    "app": "notepad"
  },
  "raw": "{\n  \"action\": \"open_app\",\n  \"parameters\": {\n    \"app\": \"notepad\"\n  }\n}"
}

--------------------------------------------------------------------------

[Agent] 执行操作: open_app { app: 'notepad' }

比如说这里我要打开notepad。 看看给的提示词中,关于我要打开的工具的描述:

  • open_app: 打开本地已安装的应用程序,如 notepad、calc、cmd 等

  • app: 应用名称或路径,如 notepad、calc、mspaint、cmd、explorer

  • args: 传递给应用的参数(可选)

这里提示词给的大模型的限定参数就很好。输出时的结果就很准确。

workFlow工作流

这里简单延申下工作流的使用,上面知识单纯的打开一个工具,那么如果我们还要接连做几个动作怎么办?其中还有循环判断,存库操作,把这些连起来就是工作流了。

这里简单延申下,打开notepad后再输入两行文字。

效果

这里主要是再agent.js文件上加了三个函数,

|-----------------|-------------------------------|
| type_text | 模拟键盘输入文字(基于 Windows SendKeys) |
| wait | 等待指定秒数(给应用启动时间) |
| click | 模拟鼠标点击屏幕位置 |

复制代码
/**
   * 模拟键盘输入文字(Windows SendKeys)
   *
   * 特殊键标记:
   *   {ENTER} = 回车    {TAB} = 制表    {SPACE} = 空格
   *   {BACKSPACE} = 退格   {DELETE} = 删除键
   *   {UP} {DOWN} {LEFT} {RIGHT} = 方向键
   *   {HOME} {END} = 行首/行尾   {PGUP} {PGDN} = 翻页
   *   {F1}~{F12} = 功能键
   *   {CTRL+A} = 全选   {CTRL+C} = 复制   {CTRL+V} = 粘贴
   *   {CTRL+S} = 保存   {ALT+F4} = 关闭窗口
   *   {SHIFT+HOME} = 选中到行首   {SHIFT+END} = 选中到行尾
   *   { } = 左右花括号需要用 {{ 和 }} 表示
   */
  type_text: {
    description: '在**当前活动窗口**中模拟键盘输入文字。支持特殊键如 {ENTER} 换行、{TAB} 制表、{CTRL+A} 全选等。常用于打开记事本后自动输入内容。',
    parameters: {
      text: '要输入的文字内容,支持 {ENTER} {TAB} {CTRL+A} 等特殊键',
      delay: '每个字符之间的延迟毫秒数(默认 10ms,太快可能丢失字符)',
      window_title: '目标窗口标题关键字(可选,用于先激活窗口)',
      pre_wait: '输入前等待的秒数(给窗口聚焦时间),默认 0.5 秒'
    },
    async execute(params) {
      let text = params.text || '';
      const preWait = parseFloat(params.pre_wait) || 0.5;
      const windowTitle = params.window_title;

      if (!text) throw new Error('缺少 text 参数');

      // 等待窗口聚焦
      if (preWait > 0) await sleep(preWait * 1000);

      // 如果需要激活特定窗口
      if (windowTitle) {
        try {
          await runPowerShell(`
Add-Type -AssemblyName System.Windows.Forms
$proc = Get-Process | Where-Object { $_.MainWindowTitle -like "*${windowTitle}*" } | Select-Object -First 1
if ($proc) {
  [System.Windows.Forms.SendKeys]::SendWait("%")  # Alt 键唤醒窗口
  Start-Sleep -Milliseconds 200
}`);
        } catch (e) {
          console.log('[type_text] 窗口激活失败,继续尝试:', e.message);
        }
      }

      // SendKeys 中的特殊字符需要转义
      // {} 用于特殊键,所以字面量的花括号需要转义为 {{ }}
      let sendKeysText = text
        // 先转义实际的花括号(用户想输入 { 和 })
        .replace(/\{/g, '{{')
        .replace(/\}/g, '}}')
        // 恢复 SendKeys 特殊键标记(把 {{ENTER}} 变回 {ENTER})
        // 先还原双重转义后的特殊键
        ;

      // 上面把 {ENTER} 变成了 {{ENTER}},需要还原
      // 但 {{ 是字面量 {,所以 {ENTER} → {{ENTER}} 后需要把特殊键还原
      // 用正则匹配常见的 SendKeys 标记并还原
      const specialKeys = ['ENTER','TAB','SPACE','BACKSPACE','DELETE','HOME','END','PGUP','PGDN',
        'UP','DOWN','LEFT','RIGHT','F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12',
        'CTRL+A','CTRL+C','CTRL+V','CTRL+X','CTRL+S','CTRL+Z','ALT+F4',
        'SHIFT+HOME','SHIFT+END'];
      
      // 把 {{ENTER}} 还原为 {ENTER}
      for (const key of specialKeys) {
        sendKeysText = sendKeysText.split(`{{${key}}}`).join(`{${key}}`);
      }

      // 用 -EncodedCommand 方式执行 PowerShell,彻底避开引号转义
      await runPowerShell(`
Add-Type -AssemblyName System.Windows.Forms
Start-Sleep -Milliseconds 500
[System.Windows.Forms.SendKeys]::SendWait("${sendKeysText}")
`);

      return { success: true, message: `已输入文字(${text.length} 个字符)`, input: text.substring(0, 50) + (text.length > 50 ? '...' : '') };
    }
  },

  /**
   * 模拟鼠标点击
   */
  click: {
    description: '在屏幕指定位置模拟鼠标点击,或点击指定窗口的某个位置',
    parameters: {
      x: '屏幕 X 坐标(像素)',
      y: '屏幕 Y 坐标(像素)',
      button: '鼠标按钮:left、right、middle,默认 left',
      double: '是否双击,默认 false',
      window_title: '目标窗口标题(可选,用于相对坐标)'
    },
    async execute(params) {
      const x = parseInt(params.x) || 0;
      const y = parseInt(params.y) || 0;
      const button = params.button || 'left';
      const isDouble = params.double === 'true' || params.double === true;

      // 用 PowerShell 移动鼠标并点击
      const psScript = `
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x}, ${y})
Start-Sleep -Milliseconds 200
$mouse = [System.Windows.Forms.MouseButtons]::${button === 'right' ? 'Right' : button === 'middle' ? 'Middle' : 'Left'}
`;

      await execAsync(`powershell -Command "${psScript.replace(/\n/g, '; ').replace(/"/g, '\"')}"`, { timeout: 10000 });

      return { success: true, message: `已${isDouble ? '双击' : '点击'} (${x}, ${y})` };
    }
  },

  /**
   * 等待一段时间
   */
  wait: {
    description: '等待指定秒数,用于给应用启动、页面加载留出时间',
    parameters: {
      seconds: '等待秒数(默认 2)',
      reason: '等待原因(可选,用于日志)'
    },
    async execute(params) {
      const seconds = parseFloat(params.seconds) || 2;
      const reason = params.reason || '';
      await sleep(seconds * 1000);
      return { success: true, message: `已等待 ${seconds} 秒${reason ? '(' + reason + ')' : ''}` };
    }
  },

也就是,分成了三个步骤,一个是打开app,然后等一下,再写入内容。

那怎么区分这三个步骤呢?怎么拿到这三个步骤呢?

毫无疑问都是大模型返回给我们的格式化结构数据。

那怎么拿到这个格式化结构数据呢,就需要修改我们的提示词了。提示词给出AI需要返回的多步骤指令。

复制代码
// ========== 2. 意图识别 ==========

function buildSystemPrompt() {
  const toolDescriptions = Object.entries(TOOLS).map(([name, tool]) => {
    const paramDesc = Object.entries(tool.parameters || {})
      .map(([key, desc]) => `    - ${key}: ${desc}`).join('\\n');
    return `- ${name}: ${tool.description}\\n${paramDesc}`;
  }).join('\\n\\n');

  return `你是一个电脑助手,负责将用户的自然语言指令转化为电脑操作。

## 可用操作

${toolDescriptions}

## 输出格式

用户指令可能包含**多个步骤**(如"打开记事本输入两段文字"),你必须用 steps 数组:

### 单步指令
{"action": "操作名称", "parameters": {"参数名": "参数值"}}

### 多步指令( steps 数组按顺序执行)
{"steps": [
  {"action": "open_app", "parameters": {"app": "notepad", "wait": "2"}},
  {"action": "wait", "parameters": {"seconds": "1", "reason": "等待记事本窗口出现"}},
  {"action": "type_text", "parameters": {"text": "第一段文字内容{ENTER}{ENTER}第二段文字内容"}}
]}

## 规则

1. 支持多步骤时优先用 steps 数组,按正确顺序排列
2. 用 type_text 输入文字前,必须先 open_app 打开应用,并加 wait 等待窗口出现
3. {ENTER} 表示回车换行,{TAB} 表示制表符
4. 只输出 JSON,不要任何解释或 markdown 标记
5. 如果用户只是闲聊,用 action: "chat"`;
}

/**
 * 解析用户指令
 */
async function parseIntent(userInput) {
  const systemPrompt = buildSystemPrompt();
      console.log("组装的系统提示词:", systemPrompt)
  const response = await ollama.chat(userInput, [], systemPrompt);
  console.log("大模型回复内容: ", response)
  // 提取 JSON
  let jsonStr = response.trim();
  if (jsonStr.startsWith('\\`\\`\\`json')) {
    jsonStr = jsonStr.replace(/\\`\\`\\`json\\n?/, '').replace(/\\n?\\`\\`\\`/, '').trim();
  } else if (jsonStr.startsWith('\\`\\`\\`')) {
    jsonStr = jsonStr.replace(/\\`\\`\\`\\n?/, '').replace(/\\n?\\`\\`\\`/, '').trim();
  }
  const jsonMatch = jsonStr.match(/\\{[\\s\\S]*\\}/);
  if (jsonMatch) jsonStr = jsonMatch[0];

  try {
    const parsed = JSON.parse(jsonStr);

    // 处理多步骤
    if (parsed.steps && Array.isArray(parsed.steps)) {
      // 验证每个步骤
      for (const step of parsed.steps) {
        if (!TOOLS[step.action]) throw new Error(`未知操作:${step.action}`);
      }
      return { steps: parsed.steps, raw: response, isMultiStep: true };
    }

    // 处理单步
    if (!TOOLS[parsed.action]) throw new Error(`未知操作:${parsed.action}`);
    return {
      steps: [{ action: parsed.action, parameters: parsed.parameters || {} }],
      raw: response,
      isMultiStep: false
    };
  } catch (e) {
    console.error('JSON 解析失败:', e.message);
    console.error('AI 原始回复:', response);
    return {
      steps: [{ action: 'chat', parameters: { reply: '抱歉没理解,能再说清楚一点吗?' } }],
      raw: response,
      parseError: e.message
    };
  }
}

运行日志:

复制代码
[Agent] 收到指令: 打开记事本,输入第一段文字"Hello World",然后换行,再输入第二段文字"这是第二段文字"
组装的系统提示词: 你是一个电脑助手,负责将用户的自然语言指令转化为电脑操作。

## 可用操作

- open_browser: 打开浏览器(Edge/Chrome)并访问指定网址或搜索内容\n    - url: 要访问的完整网址\n    - search: 搜索关键词(与 url 二选一)\n    - browser: 浏览器类型:edge 或 chrome,默认 edge\n\n- open_app: 打开本地应用程序,如 notepad、calc、mspaint、cmd 等\n    - app: 应用名称或路径,如 notepad、calc、mspaint、cmd、explorer\n    - args: 传递给应用的参数(可选)\n    - wait: 启动后等待的秒数(给窗口留出时间),默认 1 秒\n\n- type_text: 在**当前活动窗口**中模拟键盘输入文字。支持特殊键如 {ENTER} 换行、{TAB} 制表、{CTRL+A} 全选等。常用于打开记事本后自动输入内容。\n    - text: 要输入的文字内容,支持 {ENTER} {TAB} {CTRL+A} 等特殊键\n    - delay: 每个字符之间的延迟毫秒数(默认 10ms,太快可能丢失字符)\n    - window_title: 目标窗口标题关键字(可选,用于先激活窗口)\n    - pre_wait: 输入前等待的秒数(给窗口聚焦时间),默认 0.5 秒\n\n- click: 在屏幕指定位置模拟鼠标点击,或点击指定窗口的某个位置\n    - x: 屏幕 X 坐标(像素)\n    - y: 屏幕 Y 坐标(像素)\n    - button: 鼠标按钮:left、right、middle,默认 left\n    - double: 是否双击,默认 false\n    - window_title: 目标窗口标题(可选,用于相对坐标)\n\n- wait: 等待指定秒数,用于给应用启动、页面加载留出时间\n    - seconds: 等待秒数(默认 2)\n    - reason: 等待原因(可选,用于日志)\n\n- run_command: 执行系统命令(仅限安全命令)\n    - command: 要执行的命令字符串\n    - cwd: 工作目录(可选)\n\n- sys_info: 获取系统信息\n    - type: 信息类型:ip、disk、memory、cpu、all,默认 all\n\n- chat: 当用户只是聊天,不需要操作电脑时使用\n    - reply: 给用户的回复内容

## 输出格式

用户指令可能包含**多个步骤**(如"打开记事本输入两段文字"),你必须用 steps 数组:

### 单步指令
{"action": "操作名称", "parameters": {"参数名": "参数值"}}

### 多步指令( steps 数组按顺序执行)
{"steps": [
  {"action": "open_app", "parameters": {"app": "notepad", "wait": "2"}},
  {"action": "wait", "parameters": {"seconds": "1", "reason": "等待记事本窗口出现"}},
  {"action": "type_text", "parameters": {"text": "第一段文字内容{ENTER}{ENTER}第二段文字内容"}}
]}

## 规则

1. 支持多步骤时优先用 steps 数组,按正确顺序排列
2. 用 type_text 输入文字前,必须先 open_app 打开应用,并加 wait 等待窗口出现
3. {ENTER} 表示回车换行,{TAB} 表示制表符
4. 只输出 JSON,不要任何解释或 markdown 标记
5. 如果用户只是闲聊,用 action: "chat"
大模型回复内容:  {"steps": [{"action": "open_app", "parameters": {"app": "notepad"}}, {"action": "wait", "parameters": {"seconds": "1"}}, {"action": "type_text", "parameters": {"text": "Hello World{ENTER}这是第二段文字", "delay": "10"}}]}
[Agent] 解析出 3 个步骤: [
  {
    "action": "open_app",
    "parameters": {
      "app": "notepad"
    }
  },
  {
    "action": "wait",
    "parameters": {
      "seconds": "1"
    }
  },
  {
    "action": "type_text",
    "parameters": {
      "text": "Hello World{ENTER}这是第二段文字",
      "delay": "10"
    }
  }
]
[Agent] 执行步骤 1/3: open_app
[Agent] 执行步骤 2/3: wait
[Agent] 执行步骤 3/3: type_text

结束。

目前阶段还是通过模拟键盘数据来操作电脑软件的,代码实现上很麻烦,细节很多,很容易出问题。而且不同的软件操作的方式也不通用。因此最后肯定都是要向AI机器人的方向发展的。就像人一样坐在电脑面前通过视觉操作电脑。

代码地址:

https://download.csdn.net/download/csdnliuxin123524/92925807

npm install

npm run build

npm start

就能运行了。

最后记录了一个去除依赖包 压缩项目的命令:

remove-item -recurse node_modules

相关推荐
Mikowoo0071 小时前
机器学习_梯度计算
人工智能·python·机器学习
雪隐1 小时前
AI股票小助手01-量化交易基础概念
人工智能·后端·python
GISer_Jing1 小时前
Claude Code多Agent架构深度剖析
前端·人工智能·架构·自动化
小楼v1 小时前
本周AI圈炸了(4.13 - 4.19):AI纳入教师资格考核、GPT-6来了、Claude反杀、机器人跑赢了人类
人工智能·gpt·ai·机器人·热点资讯·教资·opus 4.7
芝麻开门GEO1 小时前
2026年Q2济南企业如何选择可靠的GEO服务商
大数据·人工智能·python
AI砖家1 小时前
Claude Code 跳过确认完全指南:让 AI 自己完成开发任务
前端·人工智能·python·ai编程·代码规范
YJlio1 小时前
CSDN AI数字营销实测体验:多平台账号一键分发到底好不好用?我做了一次完整实测
人工智能·windows·企业微信·火绒安全·系统备份·easyimagex
星河耀银海1 小时前
AI算力需求:不同AI场景(图像、NLP)的算力要求
人工智能·自然语言处理
Lethehong1 小时前
拒绝吃灰!手把手教你把“全能AI助理”无缝塞进微信/QQ,打造属于你的数字分身
人工智能·开源·蓝耘元生代·蓝耘maas·qwenpaw