ai agent --- 实现 openclaw 定时间效果

一. Openclaw 的定时需求

给大模型说:你帮我定个闹钟,定到9:00,我要起床去上班。

第二天,你手机的闹钟就响了。

分析openclaw的代码,我们发现它走的是以下流程:

最主要的就是这个tool

在大模型里面有个定时器,帮你盯着时间。还有一个网络信息搜索tool,帮你检索网上相关资讯。还有一个发送邮件的tool。以上三个通过字面意思就能理解。但是数据库呢?

为什么要用到数据库?

首先,发送邮件的邮件地址需要维护不?

你在哪里,什么时间段的信息,具体是哪个国家地区的信息,哪种信息。

其次是几点发送资讯信息,还有昨天给都发了哪些?哪些是你喜欢的,哪些是你不喜欢的,今天又发送了哪些?是不是都该记住,为明天更准确地为主人服务做准备?

二.创建nest项目,开发人员查询的tool

执行命令创建项目,并按照相关的包

js 复制代码
nest new cron-job-tool
cd cron-job-tool
pnpm install @langchain/core @langchain/openai zod @nestjs/config

我在# ai agent ---nest+langchain实现SSE推送信息里面已经说了nestenv 文件,下面就不再赘述,默认系统已经引入了 .env 文件。

1.用invoke搜索数据库里面的用户信息

invoke是一次性调用大模型获取数据。

现在直接在ai.service.ts 文件里面写一个runChain 方法,他的作用就是根据要求找出database对应用户的信息。

js 复制代码
import { Inject, Injectable } from '@nestjs/common';
import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import {
  AIMessage,
  BaseMessage,
  HumanMessage,
  SystemMessage,
  ToolMessage,
} from '@langchain/core/messages';
import { z } from 'zod';
import { Runnable } from '@langchain/core/runnables';

const database: any = {
  users: {
    '001': {
      id: '001',
      name: '张三',
      email: 'zhangsan@example.com',
      role: 'admin',
    },
    '002': { id: '002', name: '李四', email: 'lisi@example.com', role: 'user' },
    '003': {
      id: '003',
      name: '王五',
      email: 'wangwu@example.com',
      role: 'user',
    },
  },
};

const queryUserArgsSchema = z.object({
  userId: z.string().describe('用户 ID,例如:001, 002, 003'),
});

type QueryUserArgs = {
  userId: string;
};

const queryUserTool = tool(
  async ({ userId }: QueryUserArgs) => {
    const user = database.users[userId];

    if (!user) {
      return `用户 ID ${userId} 不存在。可用的 ID: 001, 002, 003`;
    }

    return `用户信息:\n- ID: ${user.id}\n- 姓名: ${user.name}\n- 邮箱: ${user.email}\n- 角色: ${user.role}`;
  },
  {
    name: 'query_user',
    description:
      '查询数据库中的用户信息。输入用户 ID,返回该用户的详细信息(姓名、邮箱、角色)。',
    schema: queryUserArgsSchema,
  },
);

@Injectable()
export class AiService {
  private readonly modelWithTools: Runnable<BaseMessage[], AIMessage>;

  // 获取model
  constructor(@Inject('CHAT_MODEL') model: ChatOpenAI) {
    this.modelWithTools = model.bindTools([queryUserTool]);
  }

  async runChain(query: string): Promise<string> {
    const messages: BaseMessage[] = [
      new SystemMessage(
        '你是一个智能助手,可以在需要时调用工具(如 query_user)来查询用户信息,再用结果回答用户的问题。',
      ),
      new HumanMessage(query),
    ];

    while (true) {
      const aiMessage = await this.modelWithTools.invoke(messages);
      messages.push(aiMessage);

      const toolCalls = aiMessage.tool_calls ?? [];

      // 没有要调用的工具,直接把回答返回给调用方
      if (!toolCalls.length) {
        return aiMessage.content as string;
      }

      // 依次执行本轮需要调用的所有工具
      for (const toolCall of toolCalls) {
        const toolCallId = toolCall.id || '';
        const toolName = toolCall.name;

        if (toolName === 'query_user') {
          const args = queryUserArgsSchema.parse(toolCall.args);
          const result = await queryUserTool.invoke(args);

          messages.push(
            new ToolMessage({
              tool_call_id: toolCallId,
              name: toolName,
              content: result,
            }),
          );
        }
      }
    }
  }
}

controller文件里面定义接口

js 复制代码
import { Controller, Get, Query, Sse } from '@nestjs/common';
import { AiService } from './ai.service.js';
import { Observable, from, map } from 'rxjs';

@Controller('ai')
export class AiController {
  constructor(private readonly aiService: AiService) {}

  @Get('chat')
  async chat(@Query('query') query: string) {
    const answer = await this.aiService.runChain(query);
    return answer;
  }
}

启动项目测试

js 复制代码
npm run start:dev

2.用stream搜索数据库里面的用户信息

stream是流式调用,一边生产,一边返回数据。

ai.service.ts文件里面添加runChainStream的方法如下:

js 复制代码
  async *runChainStream(query: string): AsyncGenerator<string> {
    const messages: BaseMessage[] = [
      new SystemMessage(
        '你是一个智能助手,可以在需要时调用工具(如 query_user)来查询用户信息,再用结果回答用户问题。',
      ),
      new HumanMessage(query),
    ];

    while (true) {
      const stream = await this.modelWithTools.stream(messages);
      let fullAIMessage: any = null;

      for await (const chunk of stream) {
        fullAIMessage = fullAIMessage ? fullAIMessage.concat(chunk) : chunk;

        const hasToolCallChunk =
          !!fullAIMessage.tool_call_chunks &&
          fullAIMessage.tool_call_chunks.length > 0;

        if (!hasToolCallChunk && chunk.content) {
          yield chunk.content as string;
        }
      }
      if (!fullAIMessage) {
        return;
      }
      messages.push(fullAIMessage);
      const toolCalls = fullAIMessage.tool_calls ?? [];
      if (!toolCalls.length) {
        return;
      }

      for (const toolCall of toolCalls) {
        const toolCallId = toolCall.id || '';
        const toolName = toolCall.name;

        if (toolName === 'query_user') {
          const args = queryUserArgsSchema.parse(toolCall.args);
          const result = await queryUserTool.invoke(args);

          messages.push(
            new ToolMessage({
              tool_call_id: toolCallId,
              name: toolName,
              content: result,
            }),
          );
        }
      }
    }
  }

然后在controller里面添加接口

js 复制代码
  @Sse('chat/stream')
  chatStream(@Query('query') query: string): Observable<{ data: string }> {
    const stream = this.aiService.runChainStream(query);

    return from(stream).pipe(map((chunk) => ({ data: chunk })));
  }
}

具体如下:

启动项目测试

对比两个方法

从上面的对比我们可以看到

  1. const aiMessage = await this.modelWithTools.invoke(messages); 一次性可以拿到所有的返回数据,然后找到对应工具去处理,处理以后返回给用户就好。
  2. const stream = await this.modelWithTools.stream(messages); 是生成一个数据就返回一个数据,需要把这些chunk包拼接在一起,才能组成一个完整的答案。用 fullAIMessage = fullAIMessage.concat(chunk) 把流式片段拼成一个完整 AI 消息对象,因为工具调用信息可能分片到达。

3.将人员tool提取到其他文件里面

原来,关于人员的增删改查都是放在ai.service.ts里面的,这样代码显得比较凌乱。

我们进行优化,就是将 queryUserTool的这部分业务拆分出去。

在建立一个文件user.service.ts--创建 UserService 类,理由有一个数据users,对users进行增删改查。目录如下

js 复制代码
import { Injectable } from '@nestjs/common';

type User = {
  id: string;
  name: string;
  email: string;
  role: string;
};

@Injectable()
export class UserService {
  private readonly users = new Map<string, User>([
    [
      '001',
      { id: '001', name: '张三', email: 'zhangsan@example.com', role: 'admin' },
    ],
    [
      '002',
      { id: '002', name: '李四', email: 'lisi@example.com', role: 'user' },
    ],
    [
      '003',
      { id: '003', name: '王五', email: 'wangwu@example.com', role: 'user' },
    ],
    [
      '004',
      { id: '004', name: '赵六', email: 'zhaoliu@example.com', role: 'user' },
    ],
    [
      '005',
      { id: '005', name: '孙七', email: 'sunqi@example.com', role: 'user' },
    ],
    [
      '006',
      { id: '006', name: '周八', email: 'zhouba@example.com', role: 'user' },
    ],
  ]);

  findAll(): User[] {
    return Array.from(this.users.values());
  }

  findById(id: string): User | undefined {
    return this.users.get(id);
  }

  create(user: User): User {
    this.users.set(user.id, user);
    return user;
  }

  update(id: string, partial: Partial<Omit<User, 'id'>>): User | undefined {
    const existing = this.users.get(id);

    if (!existing) {
      return undefined;
    }

    const updated: User = {
      ...existing,
      ...partial,
      id: existing.id,
    };

    this.users.set(id, updated);

    return updated;
  }

  remove(id: string): boolean {
    return this.users.delete(id);
  }
}

然后在ai.module.ts 里面注册

js 复制代码
    {
      provide: 'QUERY_USER_TOOL',
      useFactory: (userService: UserService) => {
        const queryUserArgSchema = z.object({
          userId: z.string().describe('用户ID, 例如001,002,003'),
        });

        return tool(
          async ({ userId }: { userId: string }) => {
            const user = await userService.findById(userId);
            if (!user) {
              return '未找到用户: ${userId}';
            }

            return `用户信息:\n- ID: ${user.id}\n- 姓名: ${user.name}\n- 邮箱: ${user.email}\n- 角色: ${user.role}`;
          },
          {
            name: 'query_user',
            description:
              '查询数据库中的用户信息。输入用户 ID,返回该用户的详细信息(姓名、邮箱、角色)。',
            schema: queryUserArgSchema,
          },
        );
      },
      inject: [ConfigService],
    },

在ai.service.ts里面修改原来的代码,先在构造函数里面注入QUERY_USER_TOOL

修改完之后跑一下

三. 开发发送邮件的tool

首先获取你邮箱的授权码,步骤如下:打开有设置,找到安全上设置,点击生成授权码。

邮箱码:xytaprstkhxhbfhg

相关推荐
码云之上7 小时前
换模型之后,Chatbot 为什么要自己做 compact?
前端·agent·前端工程化
星栈7 小时前
自定义 UI-UX-Pro-Max 的 CSV 知识库,把 AI 生成的后台拉回行业该有的样子
前端·agent·weui
研☆香8 小时前
简单的图片上传 删除 预览
前端
ITresearchGuest8 小时前
我用 AI 一小时写了一个世界杯数据可视化平台|前端 VibeCoding 初体验
前端·人工智能·信息可视化
慧一居士9 小时前
Vite项目中使用Less步骤,详细使用示例
前端·css
花间相见9 小时前
【LangChain组件03】—— LangChain Agents 执行与状态:工作流程与状态管理实战
java·前端·langchain
kyriewen10 小时前
面试官让我用 AI 重构一个 8 年陈的 React 组件——他说他不看代码,只看我会不会拆
前端·javascript·面试
计算机魔术师10 小时前
700吉瓦「幽灵用电」:美国AI数据中心的抢电闹剧正在失控
前端
IT_陈寒10 小时前
JavaScript数组排序踩的坑,差点让我加班到凌晨
前端·人工智能·后端