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/

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
SawanaHSN
作者分享
AI 超级智能体 Day 2 复习了 AI 超级智能体项目,同步构建 NextJS 的复刻项目。研究了阿里云百炼接口文档,以及 “NextJS 的 spring ai”,vercel 的 ai sdk 库。实现了基本的 ai 调用和生成文字 completion 功能。 1. [Github仓库](https://github.com/waitlistSawana/ai-super-agent-yupi-nextjs) 和 [上线地址](https://ai-super-agent-yupi-nextjs.hsawana9.com/)。 2. 基于阿里云百炼的 openai 兼容接口,参考 ai sdk provider 封装模型调用方法。 3. 集成 Shadcn UI 的 Registry,如果是集成了 shadcn ui 的项目,可以通过两行命令,轻松集成代码。 收获: 1. 学习封装 ai sdk 的 ai provider 2. 学习和实现 Shadcn UI 的 Registry 欢迎大家使用我的 Registry,推荐用集成了 ShadcnUI 的 NextJS 或者其他 React 框架: ``` # Init Shadcn UI # 初始化 Shadcn UI pnpm dlx shadcn@latest init # Add dashscope provider with vercel ai sdk # 增加阿里云百炼 ai 调用函数,通过 vercel ai sdk # create file - ./lib/ai/providers/dashscop.ts pnpm dlx shadcn@latest add https://ai-super-agent-yupi-nextjs.hsawana9.com/r/dashscrop-ai-provider.json # Add api route and component for generate text # 增加 api 后端端点和组件,使用阿里云百炼 # create files - ./components/generate-text.tsx - ./app/api/v1/generate-text/route.ts pnpm dlx shadcn@latest add https://ai-super-agent-yupi-nextjs.hsawana9.com/r/nextjs-generate-text-api-and-component-for-dashscrop-ai-provider.json ```
6
AI 超级智能体项目 day 1 第一次启动了 java 项目,在 vscode / trae,直接克隆 spring ai alibaba 的仓库 1. 克隆 2. 获取 api key 3. 命令启动 收获: - pom.xml == package.josn:之前是 js 全栈,迁移理解,都是包管理器,只是用到 xml 语法 - java 主程序:@SpringBootApplication - java controller / api 端点:@RestController @RequestMapping("/helloworld") (我勒个注解驱动的框架(误)) 常用命令: ```bash # 1. 首次运行:安装依赖并构建项目 mvn clean install # 2. 启动项目 mvn spring-boot:run # 其他常用命令 # 清理项目 mvn clean # 编译项目 mvn compile # 打包项目 mvn package # 安装到本地仓库 mvn install ```
6
用户中心上线(NextJS改造)
9
力扣:手搓二分查找 取中间值,代入判断,更新待选结果,更新上下界 --- 排序 取上下界限定待选值范围 while 下界 <= 上界: (# 仍有待选值) 中间值 = (下界 + 上界)// 2 if 中间值满足约束: 存为待选结果 更新上界为 中间值 - 1 (# 排除中间值和更偏离的值) else: 更新下界为 中间值 + 1 (# 排除中间值和更偏离值) return 待选结果
3
力扣 - k个有序链表重排序: 思路是 while 循环中比较每一个子链表的头选出最小的那个。 重点在于熟悉链表的概念以及创建和更新 - 理解链表和列表区别和操作: - 列表是选中了存储中的一团数据(连续的区域),一次性拿到所有值,创建和更新都是整团整团地更新,可以通过索引随机地访问里面的值,或者批量处理。 - 链表是选中了一枚数据,只有那一个节点的值+指向下一个节点的指针,所以没办法一次性知道链表的所有值,需要 list node.next.next...value 往下访问(递归?)。这也导致头尾的增删很快(只访问一枚数据),而且在存储中是游离分布的。 - 新建虚拟头部 dummy,返回时返回 dummy.next(),就把头部去掉了。 - current = dummy 后,这两个都指向同一个“实际”节点(dummy),current.next 会让 dummy.next 同步变化。 - current 的更新,使其始终代表最末端节点,current = current.next。
4
下载 APP