mcpField
C
convex-agents

convex-agents

github.com/waynesuttonAgent 工作流4051.5k 次安装4 周前更新
代码仓库

关于 convex-agents

用于通过 Convex 和 `@convex-dev/agent` 构建聊天或工具调用 Agent 的技能。它将对话历史存入 Convex,在 action 中调度模型调用,并支持响应式消息列表、工具、检索和持久化多步任务。

主要功能

  1. 01持久化对话线程
  2. 02响应式消息列表
  3. 03通过工具调用查询与修改函数
  4. 04文档检索工作流
  5. 05持久化多步 LLM 任务

适用场景

  1. 01添加带历史记录的聊天功能
  2. 02向客户端流式传送回复
  3. 03让 LLM 通过工具调用应用函数
  4. 04构建文档问答流程

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/waynesutton/builder-skills --skill convex-agents

安装后在 Claude Code 中输入/convex-agents确认已加载。

详细介绍

Convex 智能体

使用 @convex-dev/agent 构建聊天或工具调用 Agent,在线程历史记录中保存对话,并为 UI 提供响应式消息列表。唯一的规则:每次大模型调用都必须在一个 action 中运行。Mutation 会保存提示词并安排 action;它们绝不会调用模型。

适用场景

  • 添加具有持久对话历史记录的聊天助手
  • 让大模型通过工具调用你的查询和 mutation
  • 将模型回复流式传输给一个或多个客户端
  • 基于你自己的文档回答问题(RAG)
  • 将多个大模型步骤串联成可在重启后继续运行的持久化任务

安装并注册

npm install @convex-dev/agent ai @ai-sdk/openai zod
npx convex env set OPENAI_API_KEY sk-...
// convex/convex.config.ts
import { defineApp } from "convex/server";
import agent from "@convex-dev/agent/convex.config";

const app = defineApp();
app.use(agent);
export default app;

先运行一次 npx convex dev,以便在定义 Agent 之前生成 components.agent。

定义 Agent

// convex/agent.ts
import { Agent, stepCountIs } from "@convex-dev/agent";
import { openai } from "@ai-sdk/openai";
import { components } from "./_generated/api";

export const supportAgent = new Agent(components.agent, {
  name: "Support Agent",
  languageModel: openai.chat("gpt-4o-mini"),
  instructions: "You are a support assistant. Answer briefly and cite docs when possible.",
  // Lets the model call tools and then respond, up to 5 steps
  stopWhen: stepCountIs(5),
});

name 会为每条保存的消息标记写入它的 Agent。除 name 外,所有内容都可以在每次调用时覆盖。

创建线程并生成回复

在 mutation 中保存用户提示词,然后安排一个生成回复的内部 action。订阅该线程的客户端会看到新消息,无需 action 返回任何内容。

// convex/chat.ts
import { v } from "convex/values";
import { mutation, internalAction, QueryCtx, MutationCtx } from "./_generated/server";
import { components, internal } from "./_generated/api";
import { saveMessage } from "@convex-dev/agent";
import { supportAgent } from "./agent";

// Throws unless the signed in user owns the thread
async function authorizeThreadAccess(ctx: QueryCtx | MutationCtx, threadId: string) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) throw new Error("Not authenticated");
  const thread = await ctx.runQuery(components.agent.threads.getThread, { threadId });
  if (!thread || thread.userId !== identity.subject) throw new Error("Unauthorized");
}

export const startThread = mutation({
  args: {},
  returns: v.string(),
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");
    const { threadId } = await supportAgent.createThread(ctx, { userId: identity.subject });
    return threadId;
  },
});

export const sendMessage = mutation({
  args: { threadId: v.string(), prompt: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    await authorizeThreadAccess(ctx, args.threadId);
    const { messageId } = await saveMessage(ctx, components.agent, {
      threadId: args.threadId,
      prompt: args.prompt,
    });
    await ctx.scheduler.runAfter(0, internal.chat.generateReply, {
      threadId: args.threadId,
      promptMessageId: messageId,
    });
    return null;
  },
});

export const generateReply = internalAction({
  args: { threadId: v.string(), promptMessageId: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // promptMessageId makes retries safe: the same prompt is reused, never duplicated
    await supportAgent.generateText(
      ctx,
      { threadId: args.threadId },
      { promptMessageId: args.promptMessageId },
    );
    return null;
  },
});

线程 ID 是字符串,而不是 v.id(...),因为该表位于组件内部。

为 UI 列出消息

// convex/chat.ts (continued)
import { paginationOptsValidator } from "convex/server";
import { listUIMessages } from "@convex-dev/agent";
import { query } from "./_generated/server";

export const listMessages = query({
  args: { threadId: v.string(), paginationOpts: paginationOptsValidator },
  handler: async (ctx, args) => {
    await authorizeThreadAccess(ctx, args.threadId);
    return await listUIMessages(ctx, components.agent, args);
  },
});
// src/Chat.tsx
import { useUIMessages } from "@convex-dev/agent/react";
import { api } from "../convex/_generated/api";

function Chat({ threadId }: { threadId: string }) {
  const { results, status, loadMore } = useUIMessages(
    api.chat.listMessages,
    { threadId },
    { initialNumItems: 20 },
  );
  return (
    <div>
      {results.map((m) => (
        <div key={m.key} data-role={m.role}>{m.text}</div>
      ))}
      {status === "CanLoadMore" && <button onClick={() => loadMore(20)}>Older</button>}
    </div>
  );
}

listUIMessages 会将工具调用及其后的助手文本合并为一个 UIMessage,让渲染保持简单。

一个工具

使用 createTool 定义工具,它会接收一个 ctx,其中包含 runQuery、runMutation、userId 和 threadId。为处理程序的返回类型添加注解,避免循环类型错误。

// convex/tools.ts
import { createTool } from "@convex-dev/agent";
import { z } from "zod";
import { api } from "./_generated/api";

export const searchOrders = createTool({
  description: "Find the current user's orders that match a search term",
  args: z.object({
    term: z.string().describe("Product name or order number to look for"),
  }),
  handler: async (ctx, args): Promise<Array<{ id: string; status: string }>> => {
    return await ctx.runQuery(api.orders.search, { term: args.term });
  },
});

在构造函数中或调用时,通过 tools: { searchOrders } 将其传递给 Agent。有关工具错误处理、带闭包的运行时工具,以及向客户端进行增量流式传输,请参阅 references/tools-and-streaming.md。

检索与多步骤任务

有关嵌入文档、使用 @convex-dev/rag 或手动构建的向量索引搜索文档、将结果注入提示词,以及将多个大模型步骤作为持久化的 @convex-dev/workflow 任务运行,请参阅 references/rag-and-workflows.md。

常见错误

错误导致问题的原因应采取的做法
在 mutation 中调用 generateTextMutation 不能进行网络调用,而且必须是确定性的使用 saveMessage 保存提示词,并安排一个 internalAction
将 v.id("threads") 用作线程 ID线程表位于组件内部,因此其外部的 ID 是字符串使用 v.string()
在 app.use(agent) 后跳过 npx convex dev未生成 components.agent,因此类型检查会失败编写 Agent 代码前先运行一次 dev
将回复文本从 action 返回给客户端客户端断开连接时会丢失回复,也无法实现响应式更新让客户端读取 listUIMessages;保存的消息会自动显示
工具的参数没有 .describe()模型会猜测每个字段的含义,导致工具调用不当描述每个 zod 字段
工具处理程序没有返回类型注解ctx.runQuery 会导致 TypeScript 循环类型错误为处理程序添加 : Promise<...>
暴露消息查询时不进行身份验证任何客户端都能读取任何线程先调用 authorizeThreadAccess 辅助函数
定义了工具但仍使用默认的 stopWhen模型调用工具后就会停止,不返回文本回复设置 stopWhen: stepCountIs(n),其中 n > 1

检查清单

  • 在 convex.config.ts 中配置 app.use(agent),并运行 npx convex dev
  • 使用 npx convex env set 存储提供商密钥,绝不将其放在客户端代码中
  • Agent 具有 name、languageModel 和 instructions
  • 在 mutation 中保存提示词,在 internalAction 中使用 promptMessageId 生成回复
  • 线程 ID 和消息 ID 的类型为 v.string()
  • 消息列表查询在调用 listUIMessages 前检查线程所有权
  • 每个工具参数都有 zod .describe(),处理程序有返回类型
  • 使用工具时设置 stopWhen: stepCountIs(n)
  • 客户端使用 useUIMessages,而不是读取 action 的返回值

文档

评论

支持 Markdown