关于 convex-agents
用于通过 Convex 和 `@convex-dev/agent` 构建聊天或工具调用 Agent 的技能。它将对话历史存入 Convex,在 action 中调度模型调用,并支持响应式消息列表、工具、检索和持久化多步任务。
主要功能
- 01持久化对话线程
- 02响应式消息列表
- 03通过工具调用查询与修改函数
- 04文档检索工作流
- 05持久化多步 LLM 任务
适用场景
- 01添加带历史记录的聊天功能
- 02向客户端流式传送回复
- 03让 LLM 通过工具调用应用函数
- 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 中调用 generateText | Mutation 不能进行网络调用,而且必须是确定性的 | 使用 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 的返回值