003 RSC
https://sdk.vercel.ai/docs/getting-started/nextjs-app-router#create-a-server-action
https://github.com/vercel/ai/discussions/1384
3.1
3.1.1 actions.tsx
/app/actions.tsx
tsx
'use server'
import { createStreamableValue } from 'ai/rsc'
import { CoreMessage, streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
export async function continueConversation(messages: CoreMessage[]) {
const result = await streamText({
model: openai('gpt-4-turbo'),
messages
})
const stream = createStreamableValue(result.textStream)
return stream.value
}
3.1.2 前端界面
我们使用 /app/alt/page.tsx
tsx
'use client'
import { type CoreMessage } from 'ai'
import { useState } from 'react'
import { continueConversation } from '../actions'
import { readStreamableValue } from 'ai/rsc'
export default function Chat() {
const [messages, setMessages] = useState<CoreMessage[]>([])
const [input, setInput] = useState('')
const handleChat = async () => {
const newMessages: CoreMessage[] = [
...messages,
{ content: input, role: 'user' }
]
setMessages(newMessages)
setInput('')
const result = await continueConversation(newMessages)
for await (const content of readStreamableValue(result)) {
setMessages([
...newMessages,
{
role: 'assistant',
content: content as string
}
])
}
}
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map((m, i) => (
<div key={i} className="whitespace-pre-wrap">
{m.role === 'user' ? 'User: ' : 'AI: '}
{m.content as string}
</div>
))}
<form
action={handleChat}
>
<input
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={e => setInput(e.target.value)}
/>
</form>
</div>
)
}
3.2 同步返回数据
3.2.1 actions.tsx
tsx
'use server'
import { createStreamableValue } from 'ai/rsc'
import { CoreMessage, streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
export async function continueConversation(messages: CoreMessage[]) {
const result = await streamText({
model: openai('gpt-4-turbo'),
messages
})
const stream = createStreamableValue(result.textStream)
const data = { test: 'hello' }
return {
message: stream.value,
data
}
}
3.2.2 页面
tsx
'use client'
import { type CoreMessage } from 'ai'
import { useState } from 'react'
import { continueConversation } from '../actions'
import { readStreamableValue } from 'ai/rsc'
export default function Chat() {
const [messages, setMessages] = useState<CoreMessage[]>([])
const [input, setInput] = useState('')
const [data, setData] = useState<any>()
const handleChat = async () => {
const newMessages: CoreMessage[] = [
...messages,
{ content: input, role: 'user' }
]
setMessages(newMessages)
setInput('')
const result = await continueConversation(newMessages)
setData(result.data)
for await (const content of readStreamableValue(result.message)) {
setMessages([
...newMessages,
{
role: 'assistant',
content: content as string
}
])
}
}
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
{messages.map((m, i) => (
<div key={i} className="whitespace-pre-wrap">
{m.role === 'user' ? 'User: ' : 'AI: '}
{m.content as string}
</div>
))}
<form
action={handleChat}
>
<input
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={e => setInput(e.target.value)}
/>
</form>
</div>
)
}
以下为之前版本
3. 方式二:使用 RSC 与 ai/rsc
在 Vercel AI SDK 更新到 3.0 版后,它推荐使用 RSC(服务端组件)来调用模型,并提供了几个 Hook 来协助我们在页面前端完成这一任务:
useActions
useUIState
useAIState
我们可以看到,它引入了两个新的概念:AIState
(对话消息)、UIState
(界面呈现)。开发者可以针对不同的消息定制相应的显示 UI。
摘译相关介绍文档如下:
引入这两个状态后,服务器端AI操作、客户端UI呈现被分离开来。这种分离让开发者可以方便地、安全地维护AI状态。与此同时,UIState 让 RSC 能以流式传输到 UI。
AIState
是 LLM 需要读取的对话上下文,以 JSON 形式表示。AIState
可以在服务器和客户端上访问或修改。UIState
是应用程序用来显示用户界面的内容。它是完全客户端状态(与useState
非常相似),可以保存由 LLM 返回的数据和用户界面元素。这种状态可以是任何东西,但无法在服务端上访问。
使用 RSC 后,它的整体概念图如下:
3.1 创建一个 RSC 实例
我们将 RSC 放在 /lib/airsc
目录下,新建相关的目录,并创建文件: /lib/airsc/actions.ts
我们将之拆解为如下四步:
第一步:导入与准备
tsx
import { OpenAI } from "openai";
import { createAI, getMutableAIState, render } from "ai/rsc";
type UIStateType = {
role: 'user' | 'assistant' | 'system' | 'function';
id: number;
display: React.ReactNode;
}
type AIStateType = {
role: 'user' | 'assistant' | 'system' | 'function';
content: string;
id?: string;
name?: string;
}
第二步:创建 OpenAI 实例
tsx
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
第三步:创建 Actions 函数,这里为 submitUserMessage()
这里的核心是 render()
函数,它根据 LLM 的返回生成页面组件。
在缺省情况下, render()
将使用React Fragment <>
标签包装 LLM 回应的文本,可参考链接中的文档。
tsx
async function submitUserMessage(userInput: string): Promise<UIStateType>{
'use server';
const aiState = getMutableAIState<typeof AI>();
// Update the AI state with the new user message.
aiState.update([
...aiState.get(),
{
role: 'user',
content: userInput,
},
]);
// The `render()` creates a generated, streamable UI.
const ui = render({
model: 'gpt-3.5-turbo-0125',
provider: openai,
messages: [
{ role: 'system', content: 'You are a flight assistant' },
...aiState.get()
],
})
return {
id: Date.now(),
role: 'assistant',
display: ui,
};
}
疑问
- 难道每次都附加上了 system prompt吗?
- 这个对话有上下文记忆吗?
如果你需要定制文本回复,将它以自定义的格式显示,可以在render()
中增加 text
键值如下:
tsx
text: ({ content, done }) => {
// When it's the final content, mark the state as done and ready for the client to access.
if (done) {
console.log("in text:", content)
aiState.done([
...aiState.get(),
{
role: "assistant",
content
}
]);
}
return <p>{content}</p>
},
第四步:使用 createAI()
创建可供调用的 AI 实例
tsx
// Define the initial state of the AI. It can be any JSON object.
const initialAIState: AIStateType[] = [];
// The initial UI state that the client will keep track of, which contains the message IDs and their UI nodes.
const initialUIState: UIStateType[] = [];
// AI is a provider you wrap your application with so you can access AI and UI state in your components.
export const AI = createAI({
actions: {
submitUserMessage
},
// Each state can be any shape of object, but for chat applications
// it makes sense to have an array of messages. Or you may prefer something like { id: number, messages: Message[] }
initialUIState,
initialAIState
});
3.2 将前端与服务端连起来
相关的界面组件需要包裹在 <AI>
context provider 之内。你需要按这里「将前端与服务端连起来」在合适的地方进行设置。
新建 /rsc/layout.ts
,代码如下:
tsx
import { AI } from "@/lib/airsc/actions";
export default function Layout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<AI>
{children}
</AI>
);
}
3.3 编写界面组件 RSCChat
如下界面组件的功能是:
- 它显示对话消息和输入框;
- 在用户输入时,进行如下三步操作
- 将用户消息加入消息串 (使用
setMessages
) - 调用服务端 Action:
submitUserMessage()
- 将返回消息加入消息串(使用
setMessages
)
- 将用户消息加入消息串 (使用
与 useChat
对比
这里暂时没有 useChat
的 isLoading
、stop
等较为方便的帮助函数。因此,如果仅仅使用文本消息对话,方式一仍较简便。
/components/rsc-chat.tsx
tsx
'use client'
import { useState } from 'react';
import { useUIState, useActions } from "ai/rsc";
import type { AI } from '@/lib/airsc/actions';
export default function RSCChat () {
const [inputValue, setInputValue] = useState('');
const [messages, setMessages] = useUIState<typeof AI>();
const { submitUserMessage } = useActions<typeof AI>();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Add user message to UI state
setMessages((currentMessages) => [
...currentMessages,
{
id: Date.now(),
role: 'user',
display: <p>{inputValue}</p>,
},
]);
// Submit and get response message
const responseMessage = await submitUserMessage(inputValue);
// Add user message to UI state
setMessages((currentMessages) => [
...currentMessages,
responseMessage,
]);
setInputValue('');
};
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
<div>
{
// View messages in UI state
messages.map((message) => (
<div key={message.id} className='flex'>
<div className='pr-2'>
{message.role === 'user' ? 'User: ' : 'AI: '}
</div>
<div >{message.display}</div>
</div>
))
}
</div>
<form onSubmit={handleSubmit}>
<input
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
placeholder="Send a message..."
value={inputValue}
onChange={(event) => {
setInputValue(event.target.value)
}}
/>
</form>
</div>
)
}
说明
如上示例代码参考了 https://sdk.vercel.ai/docs/concepts/ai-rsc 。但进行了如下改进:
submitUserMessage()
的返回值中增加了 role
。以便在前端更方便地显示成对话的形式。
返回值的类型定义是:
tsx
type UIStateType = {
role: 'user' | 'assistant' | 'system' | 'function';
id: number;
display: React.ReactNode;
}
3.4 将对话组件显示到页面上
新建 /app/rsc/page.tsx
,在其中显示 <RSCChat />
页面组件。
tsx
import RSCChat from '@/components/rsc-chat';
export default function Page() {
return <RSCChat />;
}