发布于 2026-01-06 1 阅读
0

How to Build an AI Wine Sommelier with Stream Chat SDK Limitations of the Original UX Implementation Demo Examples Future Improvement Final Thoughts DEV's Worldwide Show and Tell Challenge Presented by Mux: Pitch Your Projects!

如何使用 Stream Chat SDK 构建 AI 葡萄酒侍酒师

原始用户体验的局限性

执行

演示示例

未来改进

最后想说的话

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

人工智能聊天机器人已成为许多现代应用程序中常见的功能。然而,仍然存在一些实际问题:“我们应该如何将聊天机器人集成到我们的服务中?”

为了探究这个问题,我启动了一个实验项目:一个侍酒师聊天机器人。葡萄酒领域包罗万象,术语繁多,非常适合测试人工智能助手在引导用户满足复杂需求方面的能力,以及如何自然地将其集成到实际应用界面中。

该项目主要关注:

  • 设计一个通过实时对话推荐葡萄酒的人工智能聊天机器人
  • 将聊天机器人嵌入到实际的聊天界面中
  • 深入了解对话式用户体验,例如消息流程、上下文感知和用户参与度

从技术角度来看,我使用了 Stream Chat 来实现聊天用户界面。这款聊天 SDK开箱即用,能够处理消息传递逻辑,让我可以专注于 AI 行为和用户体验。虽然 Stream 是一款非常优秀的工具,但本文的重点并非 SDK 本身,而是如何将 AI 聊天机器人实际集成到服务中。

如果您正在考虑为您的产品添加聊天机器人,或者只是想探索一下有哪些可能性,这篇文章可能会提供一些有用的参考点。

本项目基于演示应用 Wine Butler 和人工智能生成的葡萄酒数据。诸如葡萄品种、食物搭配和原产国等信息可能无法反映真实的葡萄酒知识。

原始用户体验的局限性

Wine Butler 应用包含每款葡萄酒的元数据,例如葡萄品种、价格范围和餐酒搭配。但是,在酒单页面,用户只能看到名称、类型和价格等基本信息。要查看详细信息,他们必须导航到每款葡萄酒的页面。

原始用户体验的局限性

例如,如果用户正在寻找“20 美元以下且适合搭配肉类的葡萄酒”,目前的系统结构迫使他们手动浏览和筛选列表——这是一个耗时且不方便的过程。

为了简化用户体验,我们添加了人工智能聊天机器人和快捷启动按钮。现在,用户只需用自然语言描述自己的喜好,聊天机器人就会推荐合适的葡萄酒。之后,他们可以直接跳转到葡萄酒的详情页面。

由人工智能驱动的个性化搜索体验

这种方法克服了基于列表的导航的局限性,并提供了由人工智能驱动的更灵活、更个性化的搜索体验。

执行

该项目后端使用 Node.js 构建,前端使用 Android(Kotlin 和 Jetpack Compose)。如果您也在类似的环境中工作,我强烈建议您阅读《使用 Compose 构建 Android AI 助手》这篇文章

为了构建聊天机器人的消息传递界面,我使用了 Stream Chat SDK。Stream 的最大优势之一是它几乎提供了聊天功能所需的所有基本组件,开箱即用。这使我能够完全专注于用户与 AI 之间的交互逻辑,而无需关注底层聊天基础设施。

SDK 处理了消息流、状态同步和 UI 渲染,使我能够专注于设计聊天机器人的行为和用户体验。

AI聊天机器人流程

消息结构

为了有效地提供葡萄酒推荐,聊天机器人需要发送的不仅仅是纯文本——它还必须包含结构化数据,例如产品 ID、葡萄酒图片和消息状态指示器。

Stream Chat 支持通过 向每条消息添加自定义字段extraData,这使我能够定义如下所示的自定义消息格式:

场地 描述 类型
文本 聊天信息文本 默认字段
附件 葡萄酒图片默认字段
wine_id 推荐葡萄酒 ID 自定义字段
人工智能生成的 表明该消息是否由人工智能生成。 自定义字段
生成 指示邮件是否仍在编写中 自定义字段

在 Kotlin 中,您可以像这样访问这些自定义字段:

val Message.wineId: String? get() = extraData["wine_id"] as? String
Enter fullscreen mode Exit fullscreen mode

构建聊天用户界面和自定义消息

Stream Chat SDK 提供预构建的 UI 组件,可以轻松渲染完整的聊天界面——只需指定频道 ID,即可开始使用。

@Composable
fun MessageScreen(
    cid: String,
    onBackPressed: () -> Unit
) {
    val context = LocalContext.current
    val viewModelFactory = remember {
        MessagesViewModelFactory(
            context = context,
            channelId = cid,
            messageLimit = 30
        )
    }

    BackHandler(onBack = onBackPressed)

    ChatTheme {
        MessagesScreen(
            viewModelFactory = viewModelFactory,
            onBackPressed = onBackPressed
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

预置 UI 组件 - MessagesScreen

除了渲染消息之外,我还使用 StreamChatComponentFactory来自定义单个消息的显示方式。

ChatTheme(
    componentFactory = object : ChatComponentFactory {
        ...
    }
) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

在这个项目中,我实现了两个关键的定制化改动:

  • 当人工智能正在编写消息时,显示类似“正在查找目录……”的占位符。
  • 当消息中包含葡萄酒推荐时,显示一个“查看详情”按钮,链接到产品页面。

为了支持这一点,我利用了自定义消息字段(ai_generated,,generatingwine_id),并重写了 MessageFooterContent,以便根据消息状态渲染不同的组件。

// MessageExtension.kt
val Message.wineId: String? get() = extraData["wine_id"] as? String
val Message.isAiGenerating: Boolean get() =
    extraData["ai_generated"] as? Boolean == true &&
            extraData["generating"] as? Boolean == true

// MessageScreen.kt
ChatTheme(
    componentFactory = object : ChatComponentFactory {

        @Composable
        override fun MessageFooterContent(messageItem: MessageItemState) {
            if (messageItem.message.isAiGenerating) { // custom field
                Text("Find Catalog...")
            } else {
                Column {
                    messageItem.message.wineId?.let { wineId ->  // custom field
                        Button(
                            onClick = {
                                onWineClick(wineId)
                            },
                        ) {
                            Text("See Detail")
                        }
                    }
                    super.MessageFooterContent(messageItem)
                }
            }
        }
    }
) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

消息页脚内容

这种设置使得用户界面能够根据聊天机器人的状态和消息上下文实时调整。

得益于 Stream SDK 的灵活性,我能够提供量身定制的用户体验,而无需从头开始构建复杂的聊天逻辑。

设置后端

对于后端,我参考了《使用 Compose 为 Android 构建 AI 助手》中介绍的chat-ai-sample项目,并将其适配到 Node.js 环境。

每当有用户进入聊天时,我都会AnthropicAgent在服务器端为该用户初始化一个专用的 AI 代理实例()。

为了实时检测用户消息,我使用了 Stream SDK 的事件订阅功能来监听事件message.new——这会触发 AI 响应生成工作流程。

// agentController.ts
const agent = await createAgent(user_id, channel_type, channel_id_updated);
await agent.init();

// AnthropicAgent.ts
init = async () => {
  const apiKey = process.env.ANTHROPIC_API_KEY as string | undefined;
  if (!apiKey) {
    throw new Error("Anthropic API key is required");
  }
  this.anthropic = new Anthropic({ apiKey });

  // subscribe new message event
  this.chatClient.on("message.new", this.handleMessage);
};
Enter fullscreen mode Exit fullscreen mode

跳过AI助手的消息

并非每条信息都应该触发人工智能响应。

为了避免回复聊天机器人自身生成的消息,我们使用之前定义的ai_generated标志将其过滤掉:

// AnthropicAgent.ts
private handleMessage = async (e: Event<DefaultGenerics>) => {
    ...
    if (!e.message || e.message.ai_generated) {
        console.log('Skip handling ai generated message');
        return;
    }
    ...
    const message = e.message.text;
    if (!message) return;
}
Enter fullscreen mode Exit fullscreen mode

与人工智能沟通

以下是我为处理 AI 回复而实现的消息流程:

  1. 合并最近的消息以构建对话上下文
  2. 发送一条临时占位符消息,表明人工智能正在“输入”。
  3. 使用构建的上下文向人工智能模型请求响应
  4. partialUpdate收到回复后,用于将占位符替换为最终回复。
private handleMessage = async (e: Event<DefaultGenerics>) => {
    ...
    // 1. Merge recent messages to create conversation context
    const messages = this.channel.state.messages
        .slice(-10)
        .filter((msg) => msg.text && msg.text.trim() !== '')
        .map<MessageParam>((message) => ({
        role: message.user?.id.startsWith('ai-wine-butler-from')
            ? 'assistant'
            : 'user',
        content: message.text || '',
        }));

    if (e.message.parent_id !== undefined) {
      messages.push({
        role: 'user',
        content: message,
      });
    }

    // 2. Send a temporary empty message to indicate the AI is “typing”
    const { message: channelMessage } = await this.channel.sendMessage({
      text: '',
      ai_generated: true,
      generating: true,
    });

    // 3. Make a real request to the AI model and receive a response
    const aiAgentMessage = await this.anthropic.messages.create({
      max_tokens: 1024,
      messages,
      model: 'claude-3-5-sonnet-20241022',
    });

    // 4. Use partialUpdate to replace the placeholder message with the final reply
    await this.chatClient.partialUpdateMessage(channelMessage.id, {
      set: {
        text: aiAgentMessage.content
            .filter((c) => c.type == 'text')
            .map((c) => c.text)
            .join(''),
        generating: false,
      },
    });
    ...
}
Enter fullscreen mode Exit fullscreen mode

这种设置使得聊天机器人能够以自然流畅的实时方式做出响应。通过利用 Stream 的消息事件系统,我可以检测用户输入,并根据 AI 的响应动态更新聊天内容。

构建人工智能响应:提示工程

仅仅构建一个聊天机器人并不足以使其达到生产就绪状态。

默认情况下,大多数大型语言模型充当通用助手——它们可以回答各种各样的问题,但通常缺乏特定领域的知识或对业务约束的意识。

例如,要让 AI 根据特定标准推荐葡萄酒,或者将建议限制在库存商品范围内,需要清晰明确的指示。

这就需要用到提示工程了。通过精心设计结构化的提示,我引导人工智能扮演特定角色,并以一致、结构化的格式生成回复。

角色定义:葡萄酒管家

首先,我为人工智能定义了一个清晰的人物角色:

You are a friendly AI wine butler who answers questions about wine and recommends the perfect wine for any occasion.
Enter fullscreen mode Exit fullscreen mode

提供背景信息:葡萄酒目录

大型语言模型的优势之一在于其处理结构化数据的能力。为了充分利用这一优势,我将整个葡萄酒目录以 JSON 字符串的形式传递给人工智能,使其能够仅从现有库存中推荐葡萄酒。

The wines we have in stock are as follows:
${JSON.stringify(wines, null, 2)}
Enter fullscreen mode Exit fullscreen mode

行为指导:制定详细指令

除了明确其角色之外,我还为人工智能提供了在不同情况下需要遵循的详细行为准则:

1. Answer any wine-related questions from the user.
2. Recommend wines based on the user’s preferences, occasion, or food pairing, using a courteous and elegant tone.
3. Only suggest wines that are currently in stock.
4. Provide clear and simple explanations if the user asks about wine knowledge.
5. Politely decline to answer if the question is too difficult or inappropriate.
Enter fullscreen mode Exit fullscreen mode

输出格式:强制使用 JSON 响应

我需要人工智能以严格的 JSON 格式返回响应,以便在客户端进行自定义渲染。为了强制执行此操作,我在提示符末尾添加了一条格式化规则:

All responses must be in JSON format, using escape characters like \\n for line breaks. Do not include markdown or explanations—only return the JSON.

Required fields:
- text: A conversational message including a recommendation and a follow-up question
- attachments: If a wine is recommended, include an image
- wine_id: The ID of the recommended wine (if any)

Example format:
{
  text: string,
  attachments: [
    {
      type: 'image (constant)',
      image_url: '(wine image url)'
    }
  ],
  wine_id: string?
}
Enter fullscreen mode Exit fullscreen mode

提供系统提示

为了确保人工智能行为的一致性,我在对话上下文的开头添加了完整的提示作为系统级指令。

在此实现中,系统提示信息作为列表中的第一条消息注入:

private handleMessage = async (e: Event<DefaultGenerics>) => {
    ...
    const systemPrompt = "You are a friendly AI wine butler who..."

    const messages = [
        {
        role: 'user', // or 'system' depending on your LLM setup
        content: systemPrompt,
        } as MessageParam,
        ...this.channel.state.messages
        ...
    ]
}
Enter fullscreen mode Exit fullscreen mode

解析 JSON 并更新消息

AI 的响应以 JSON 字符串的形式返回,所以我解析了它,并用它partialUpdateMessage来更新 UI 上的占位符消息:

private handleMessage = async (e: Event<DefaultGenerics>) => {
    ...
    await this.chatClient.partialUpdateMessage(channelMessage.id, {
        set: {
        ...JSON.parse(
            aiAgentMessage.content
            .filter((c) => c.type == 'text')
            .map((c) => c.text)
            .join(''),
        ),
        generating: false,
        },
    });
    ...
}
Enter fullscreen mode Exit fullscreen mode

锵锵!

人工智能的响应完全符合预期——提供了一条推荐信息、一张葡萄酒图片以及一个指向产品页面的“查看详情”按钮。所有组件协同工作,打造了流畅且连贯的用户体验。

人工智能的反应完全符合预期。

演示示例

以下是一些人工智能葡萄酒管家在实践中如何响应用户请求的真实示例。

除了基本的问答之外,该聊天机器人还能以令人印象深刻的流畅性成功处理复杂的场景,包括条件逻辑、上下文感知和多轮对话。

询问葡萄酒基础知识

询问葡萄酒基础知识

基于价格的推荐

基于价格的推荐

食物搭配建议

食物搭配建议

多条件请求

多条件请求

情境感知型后续行动

情境感知型后续行动

我先是问有没有“30 美元以下的法国葡萄酒”,然后又补充道:“其实,价格不用太在意——只要确保它和肉类搭配得好就行。”

聊天机器人记住了之前的上下文,并据此调整了推荐内容。

未来改进

虽然我已经成功构建并验证了聊天机器人的核心功能,但我希望探索一些技术改进,使该系统更适合生产环境。

响应流

ChatGPT 和 Claude 支持流式传输,允许客户端逐个接收消息。这创造了更流畅、更自然的用户体验——文本看起来像是实时输入的。

然而,在这个项目中,我要求人工智能返回格式完整的 JSON 响应。由于解析方面的限制,流式传输不可行。我希望探索如何在保持结构化格式的同时,提升人工智能响应的响应速度和用户体验。

拓展人工智能工具的使用范围

现代LLM支持工具使用内置机制,例如函数调用、API集成和数据过滤。

如果将其应用于本项目,则可解锁以下新功能:

  • 根据用户输入触发产品搜索 API
  • 通过编程方式与 Stream 聊天系统交互

有了这些新增功能,聊天机器人不仅可以回复静态文本,还能根据用户意图采取实际的、动态的操作,从而变得更加强大和互动。

最后想说的话

人工智能聊天机器人已不再是新鲜事物。但对许多团队来说,真正的挑战在于如何将它们切实地集成到产品中,以及了解一旦它们到位后真正能够实现哪些功能。

这个项目是我探索这个问题的途径。通过将人工智能助手直接嵌入到现有的用户界面中,并将提示、数据和聊天逻辑连接起来,我得以开展有针对性的实验,并快速验证哪些方法有效。

Stream Chat 等工具帮助简化了基础设施方面,使我能够专注于人工智能行为和用户体验。

我希望这篇文章能为任何考虑集成 AI 聊天机器人的人提供现实和实用的参考——无论你是准备全面推广还是仅仅从一个小型原型开始。

文章来源:https://dev.to/getstreamhq/how-to-build-an-ai-wine-sommelier-with-stream-chat-sdk-505b