AI 超级智能体笔记和作业 - AI 应用部分
项目代码
开源地址:https://github.com/waitlistSawana/ai-super-agent-yupi-nextjs 上线地址:https://ai-super-agent-yupi-nextjs.hsawana9.com/
基于 Nextjs 后端实现项目课程中提到的功能,主要参考 AI SDK 的实现,提供 Shadcn Registry 服务 快速复制代码到本地。
欢迎 star ⭐!
本章学习
- 提示词工程
- 生成文本
- 多轮对话 Chatbot
- 拦截器(中间件)
- 结构化输出
- 记忆持久化(本地文件)
- 记忆持久化(数据库)
1. 提示词工程
多步骤提示词,xml 结构的提示词。在 Dify、阿里云百炼 迭代提示词,效果非常满意:
- 角色设定
- 步骤更新
- 话题拓展
- 对话结束
- 工具调用
▼markdown复制代码<identity> <role>机器学习研究教练</role> <expertise> <area>机器学习</area> <area>数据科学</area> <area>音频处理</area> <area>计算机视觉</area> <area>自然语言处理</area> </expertise> <tone>友好、引导式、专业但平易近人</tone> </identity> <purpose> 作为您的机器学习研究伙伴,我的目标是帮助您在研究过程中取得进展。我专注于引导您完成构建基本流水线-基准模型阶段的训练前分析讨论模块,采用分步骤讨论的方式:首先认识问题,然后理解数据,接着确定研究目标,最后规划基础流水线。每个阶段我们会充分讨论并获得您的确认后,才会进入下一小节。 </purpose> <conversation_flow> <phase name="认识问题" current="true" done="false"> <focus> <point>帮助用户深入理解研究问题的本质和挑战</point> <point>分析问题的类型(分类、回归、聚类等)</point> <point>探讨问题的应用背景和实际意义</point> <point>主动提出关键问题点和可能的难点</point> </focus> <transition> <question>您对问题的理解是否清晰?我们是否可以进入下个话题,开始探索数据特征?</question> </transition> </phase> <phase name="认识数据" current="false" done="false"> <focus> <point>请求用户提供关于数据集、数据列、元数据的详细信息</point> <point>探索数据集、数据列、元数据的结构和特征,描述数据集、数据列的含义和作用</point> <point>元数据的质量、分布和潜在偏差</point> <point>元数据间的潜在关系和相互影响</point> <point>分析数据列的现实意义,预期影响以及在本课题中的意义</point> </focus> <transition> <question>您对数据的特点是否已有清晰认识?我们是否可以进入下个话题,明确研究目标?</question> </transition> </phase> <phase name="确定目标" current="false" done="false"> <focus> <point>帮助用户明确具体的研究目标</point> <point>可能的模型</point> <point>合适的评估指标</point> <point>确定表现良好和不良好的标准</point> <point>设定现实可行的阶段性目标</point> </focus> <transition> <question>您对研究目标和评估方法是否满意?我们是否可以进入下个话题,规划基础流水线?</question> </transition> </phase> <phase name="规划基准流水线" current="false" done="false"> <focus> <point>讨论最小可行的处理流程,基准模型</point> <point>建议适合的基线模型</point> <point>设计简单但完整的实验验证方案</point> </focus> <transition> <question>您对这个基础流水线的规划是否满意?是否进入下一阶段:模型训练阶段?</question> <action>结束对话,唤起结束对话工具</action> </transition> </phase> </conversation_flow> <information_verification> <action>验证用户提供的链接和资料是否可访问和充分</action> <action>检查是否能获取到研究问题和数据集的基本信息</action> <action>当信息不足时,明确向用户索要特定信息</action> <insufficient_info_response> <template> 我注意到您提供的{资源类型}似乎{问题描述}。为了更好地帮助您,我需要了解以下信息: 1. {数据集的基本结构} 2. {可用的特征、标签或元数据信息} 3. {包含数据集信息的网址链接} 4. {...具体需要的信息} 您能否提供这些信息,这样我们才能开始有效地分析研究问题? </template> </insufficient_info_response> </information_verification> <response_guidelines> <guideline>本阶段只关注最基本的流水线实现,目标是引导用户完成基准模型,不主动进行拓展和优化建议</guideline> <guideline>每次回复仅聚焦于当前阶段的内容,不要提前讨论后续小节,适当引导话题回归</guideline> <guideline>使用自然、对话式的语言,避免过于机械化的结构</guideline> <guideline>像一位经验丰富的导师一样交流,主动提出见解和分析</guideline> <guideline>在每个阶段结束时,明确询问用户是否准备进入下一阶段</guideline> <guideline>只有在用户明确确认后,才更新当前阶段并进入下一阶段</guideline> <guideline>如果用户有疑问或需要更多讨论,继续深入当前阶段的内容</guideline> <guideline>不提供任何代码实现,专注于概念和方法的讨论</guideline> <guideline>如果用户跳跃话题或提前要求代码实现,指出其在后续章节,并礼貌地引导用户回到当前小节</guideline> </response_guidelines> <phase_transition> <action>仅在用户明确确认后,将当前小节标记为完成</action> <action>将下一小节的current属性设置为"true"</action> <action>在新小节开始时,简要回顾前一小节的关键结论</action> <action>明确告知用户现在进入了新的小节</action> </phase_transition> <tools> <tool>url to markdown</tool> <tool>...orther tools</tool> </tools> <knowledge_access> <action>访问相关领域的专业知识</action> <action>提供适合用户当前阶段的信息深度</action> <action>根据研究主题提供针对性的方法和技术建议</action> </knowledge_access>
2. 生成文本
根据aisdk文档和阿里云百炼的文档自己配置了一个 dashscope provider,用到了云百炼的 openai 兼容接口,也配置了 sealos 的接口。
主要用到了 ai 和 @ai-sdk/react 包,根据文档的示例实现的。
注意:如果想用云百炼,可以用我配置好的 ai-provider,GIthub 代码 或者 用提供的 Shadcn Registry 快速安装 pnpm dlx shadcn@latest add https://ai-super-agent-yupi-nextjs.hsawana9.com/r/dashscrop-ai-provider.json
后端采用 ai provider 和 generateText、streamText
▼ts复制代码/** * generateText Route * * @author Sawana Huang * * @see https://sdk.vercel.ai/docs/ai-sdk-core/generating-text * @see https://sdk.vercel.ai/cookbook/rsc/generate-text */ import { generateText } from "ai"; import { NextResponse, type NextRequest } from "next/server"; import { sealos } from "@/lib/ai/providers/sealos"; export interface GenerateTextPostRequestBody { prompt: string; } export interface GenerateTextPostSuccessResponse { message: string; text: string; } export async function POST(request: NextRequest) { const { prompt } = (await request.json()) as GenerateTextPostRequestBody; const result = await generateText({ model: sealos("deepseek-chat-0324"), system: "You are a helpful assistant.", prompt: prompt, }); const text = result.text; return NextResponse.json( { message: "success", text: text } as GenerateTextPostSuccessResponse, { status: 200, statusText: "OK", headers: { "Content-Type": "application/json", }, }, ); }
前端可以直接 fetch 后端端点
▼ts复制代码/** * generateText compoent * * @author Sawana Huang * * @see https://sdk.vercel.ai/cookbook/rsc/generate-text * * @example use it in your component * import GenerateTextDemo from "@/components/generate-text-demo"; * * <GenerateTextDemo /> */ "use client"; import { cn } from "@/lib/utils"; import { useState } from "react"; import type { GenerateTextPostSuccessResponse } from "@/app/api/v1/generate-text/route"; type GenerateTextDemoProps = { className?: string; }; export default function GenerateTextDemo({ className, ...props }: React.ComponentProps<"div"> & GenerateTextDemoProps) { const [generation, setGeneration] = useState(""); const [isLoading, setIsLoading] = useState(false); return ( <div className={cn(className)} {...props}> <div onClick={async () => { setIsLoading(true); try { const response = await fetch("/api/v1/generate-text", { method: "POST", body: JSON.stringify({ prompt: "Why is the sky blue?", }), }); const json = (await response.json()) as GenerateTextPostSuccessResponse; setGeneration(json.text); } catch (error) { console.error("Error generating text:", error); setGeneration( "Unable to generate text at the moment. Please refresh and try again.", ); } finally { setIsLoading(false); } }} > Generate </div> {isLoading ? "Loading..." : generation} </div> ); }
3. 多轮对话 Chatbot
直接使用 ai-sdk 提供的 useChat 方法,参考 ai-sdk Chatbot。
原理是:
- 前端维护 messages,作为 body data 传送到后端,后端接收到之后传输整个 messages 给大模型。
- 后端用 generateText 或 streamText 来实现文本或者流式返回。
- 前端收集 input 组合成完整的 messages,并有完整的 hooks,如 isLoading。
- 通过 onFInish 和 onError 等调用实现更完整的业务
后端
▼ts复制代码/** * server route for comprehensive chatbot component * * @description: build server route for comprehensive chatbot component, following the document of AI SDK * * @see https://ai-sdk.dev/docs/ai-sdk-ui/chatbot * * @author Sawana Huang <hsawana9@gmail.com> * @date 2025-05-03 * */ import { dashscope } from "@/lib/ai/providers/dashscope"; import { streamText, type UIMessage } from "ai"; import { type NextRequest } from "next/server"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export interface PostRequestBody { messages: UIMessage[]; } export interface PostSuccessResponse { message: string; } export async function POST(request: NextRequest) { const { messages, // custom fields key_use_chatbot, key_handle_submit, } = (await request.json()) as PostRequestBody & { // custom fields key_use_chatbot: string; key_handle_submit: string; }; // custom fields console.log({ key_use_chatbot, key_handle_submit }); const result = streamText({ // You can use any model you want. // see: https://ai-sdk.dev/providers/ai-sdk-providers model: dashscope("deepseek-v3"), system: "You are a helpful assistant.", messages, }); return result.toDataStreamResponse({ // Error Message: // The default error message is masked "An error occurred." // You can forward error messages or send custome messages. getErrorMessage: (error) => { if (error == null) { return "unknown error"; } if (typeof error === "string") { return error; } if (error instanceof Error) { return error.message; } return JSON.stringify(error); }, // Usage Information sendUsage: true, // Reasoning: // https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#reasoning sendReasoning: true, // Source // https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#sources // some providers response include sources // eg. Perplexity, Google generative AI sendSources: true, }); }
前端:
▼tsx复制代码/** * comprehensive chatbot component * * @description: comprehensive chatbot component, following the document of AI SDK * * @see https://ai-sdk.dev/docs/ai-sdk-ui/chatbot * * @author Sawana Huang <hsawana9@gmail.com> * @date 2025-05-03 * */ "use client"; import { cn } from "@/lib/utils"; import { useChat } from "@ai-sdk/react"; type ChatBotProps = { className?: string; }; export default function ChatBot({ className, ...props }: React.ComponentProps<"div"> & ChatBotProps) { const { messages, input, handleInputChange, handleSubmit, status, stop, error, reload, } = useChat({ // API endpoint Default /api/chat api: "/api/v1/chat-bot", // Throttle the messages and data updates to 50ms: experimental_throttle: 50, // Event Callbacks: // https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#event-callbacks onFinish: (message, { usage, finishReason }) => { console.log("Finished streaming message:", message); console.log("Token usage:", usage); console.log("Finish reason:", finishReason); }, onError: (error) => { console.error("An error occurred:", error); }, onResponse: (response) => { console.log("Received HTTP response from server:", response); // You can throw error here to trigger the onError callback }, // Custom headers, body, and credentials headers: { Authorization: "Bear your_token", }, body: { key_use_chatbot: "key of useChatbot", }, credentials: "same-origin", }); return ( <div className={cn("", className)} {...props}> <h2>--- Basic Messages ---</h2> {messages.map((message) => ( <div key={message.id}> {message.role === "user" ? "User: " : "AI: "} {message.content} </div> ))} <h2>--- Messages with Reasoning ---</h2> {messages.map((message) => ( <div key={message.id}> {message.role === "user" ? "User: " : "AI: "} {message.parts.map((part, index) => { // text parts: if (part.type === "text") { return <div key={index}>{part.text}</div>; } // reasoning parts: if (part.type === "reasoning") { return ( <pre key={index}> {part.details.map((detail) => detail.type === "text" ? detail.text : "<redacted>", )} </pre> ); } })} </div> ))} <h2>--- Messages with Sources ---</h2> {messages.map((message) => ( <div key={message.id}> {message.role === "user" ? "User: " : "AI: "} {message.parts .filter((part) => part.type !== "source") .map((part, index) => { if (part.type === "text") { return <div key={index}>{part.text}</div>; } })} {message.parts .filter((part) => part.type === "source") .map((part) => ( <span key={`source-${part.source.id}`}> [ <a href={part.source.url} target="_blank"> {part.source.title ?? new URL(part.source.url).hostname} </a> ] </span> ))} </div> ))} {(status === "submitted" || status === "streaming") && ( <div> {status === "submitted" && "loading... "} <button type="button" onClick={() => stop()}> Stop </button> </div> )} <h2>--- Erorr ---</h2> {error && ( <div> <div>An error occurred.</div> <button type="button" onClick={() => reload()}> Retry </button> </div> )} <h2>--- Submit input and Button ---</h2> <form onSubmit={(event) => { handleSubmit(event, { // Custom body fieds per request body: { key_handle_submit: "key of handleSubmit", }, }); }} > <input name="prompt" value={input} placeholder="Type your message here..." onChange={handleInputChange} disabled={status !== "ready" || error != null} /> <button type="submit">Submit</button> </form> </div> ); }
4. 拦截器(中间件)
在 Spring AI 中是 Advisor 的功能,而在 ai-sdk 中,则是通过 wrapLanguageModel 的方式
原理是:
- 包裹大模型 provider,拦截大模型调用前后的过程,从而生成一个 wrapedLLM,后端直接调用这个新的模型
- 自定义 Middleware:transformParams 控制传输大模型前,wrapStream 和 wrapGenerate 分别控制大模型结果的流式和文本返回。
输出日志的自定义 middleware 示例:
▼ts复制代码/** * Implementing Language Model Middleware * * customize your own middleware * * - `transformParams`: Modify the parameters of the language model call. * - `wrapGenerate`: wrap the doGenerate function. * - `wrapStream`: wrap the doStream function. * * you can modify the paramers, call the languaage moel, * and modify the result. in other words, you can do someting * before and after the language model call. * * following are examples from ai adk */ /** * Logging Middleware */ const loggingMiddleware: LanguageModelV1Middleware = { wrapGenerate: async ({ doGenerate, params }) => { console.log("doGenerate called"); console.log(`params: ${JSON.stringify(params, null, 2)}`); const result = await doGenerate(); console.log("doGenerate finished"); console.log(`generated text: ${result.text}`); return result; }, wrapStream: async ({ doStream, params }) => { console.log("doStream called"); console.log(`params: ${JSON.stringify(params, null, 2)}`); const { stream, ...rest } = await doStream(); let generatedText = ""; const transformStream = new TransformStream< LanguageModelV1StreamPart, LanguageModelV1StreamPart >({ transform(chunk, controller) { if (chunk.type === "text-delta") { generatedText += chunk.textDelta; } controller.enqueue(chunk); }, flush() { console.log("doStream finished"); console.log(`generated text: ${generatedText}`); }, }); return { stream: stream.pipeThrough(transformStream), ...rest, }; }, }; const loggingModel = wrapLanguageModel({ model: dashscope("deepseek-v3"), middleware: loggingMiddleware, });
5. 结构化输出
使用 useObject 和 streamObject 方法
原理是:
- prompt 中提示大模型生成指定的内容
- 通过 schema 提示大模型生成的内容和格式,并进行类型审查
schema:
▼ts复制代码/** * Object Generation * * @see https://ai-sdk.dev/docs/ai-sdk-ui/object-generation */ import { z } from "zod"; // define a schema for the notifications export const notificationSchema = z.object({ notifications: z.array( z.object({ name: z.string().describe("Name of a fictional person."), message: z.string().describe("Message. Do not use emojis or links."), }), ), });
前端和后端关键代码
▼tsx复制代码/** * Object Generation * * @see https://ai-sdk.dev/docs/ai-sdk-ui/object-generation */ // 前端 // ... const { isLoading, stop, error, object, submit } = useObject({ api: "/api/v1/object-generation/notification", schema: notificationSchema, onFinish({ object, error }) { // typed object, undefined if schema validation fails: console.log("Object generation completed:", object); // error, undefined if schema validation succeeds: console.log("Schema validation error:", error); }, onError(error) { // error during fetch request: console.error("An error occurred:", error); }, }); // ... // 后端 const context = (await request.json()) as string | undefined; const result = streamObject({ model: dashscope("qwen-plus"), schema: notificationSchema, prompt: `Generate 3 notifications for a messages app in this context:` + context, }); // ...
6. 记忆持久化
分别实现了 本地文件 和 数据库 两种形式
原理是:
- 通过后端 streamText 的 onFinishi 回调捕获生成结果,把这个结果保存下来
- 本地是直接把结果写入到 .chat/*.json 中,通过文件系统管理
- 数据库则是把结果和数据库的结果相比较,存入新的的消息
后端代码
▼ts复制代码/** * Chat API for Persistence * * @see https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence */ // ... const result = streamText({ model: dashscope("qwen-turbo"), messages, // store messages async onFinish({ response }) { await saveChat({ id, messages: appendResponseMessages({ // history messages messages, // response messages responseMessages: response.messages, }), }); }, // id format for server-side messages: experimental_generateMessageId: createIdGenerator({ prefix: CHAT_MESSAGE_SERVER_PREFIX, size: 16, }), }); // ...
总结
已经完成了 Nextjs 的 AI 智能体的基本实现。
通过 Shadcn Registry 提高了这些代码的可重用性,符合 DRY 原则。模块化的组件加上 Regristry 服务,我可以非常方便地把这些代码集成到我的新项目中。
锻炼了自己的提示词工程功力,这份提示词已经非常接近可以使用的程度,效果已经和当初使用 Buildpad 时一样惊艳。xml,多步骤指令,ai 辅助生成 prompt。
计划:
- Rag 实现。本地实现和接入服务
- 提示词从 Dify 迁移到项目中。包括 提示词缓存,提示词 token 优化,工具调用,mcp 调用
更多信息,去看上线的网站吧!
有帮助的话,点点 Star 谢谢各位!
开源地址:https://github.com/waitlistSawana/ai-super-agent-yupi-nextjs 上线地址:https://ai-super-agent-yupi-nextjs.hsawana9.com/
