第 5 章:实现第一个 AI Chat 页面

0 阅读2分钟

第 5 章:实现第一个 AI Chat 页面

本章目标

这一章完成一个最小可用的 AI Chat:用户输入问题,后端调用模型,前端展示回答。

本章效果

本章完成后,页面会具备基础对话能力。后续第 6-8 章会继续加入流式输出和结构化分析。

AI Chat 流式输出转存失败,建议直接上传图片文件

数据结构设计

先定义消息类型:

export type ChatRole = "user" | "assistant" | "system";

export interface ChatMessage {
  id: string;
  role: ChatRole;
  content: string;
  createdAt: number;
}

前端不一定展示 system 消息,但服务端构造 Prompt 时会用到。

Chat API

创建 src/app/api/chat/route.ts

import { createChatModel } from "@/lib/ai/model";

interface ChatRequest {
  messages: Array<{
    role: "user" | "assistant" | "system";
    content: string;
  }>;
}

export async function POST(request: Request) {
  const body = (await request.json()) as ChatRequest;
  const model = await createChatModel();

  const result = await model.invoke([
    {
      role: "system",
      content: "你是一个专业、简洁、可靠的 AI 应用开发助手。"
    },
    ...body.messages
  ]);

  return Response.json({
    content: result.text
  });
}

前端状态

创建一个基础 Chat 组件:

"use client";

import { useState } from "react";

interface Message {
  role: "user" | "assistant";
  content: string;
}

export function ChatPanel() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);

  async function sendMessage() {
    if (!input.trim() || loading) return;

    const nextMessages: Message[] = [
      ...messages,
      { role: "user", content: input }
    ];

    setMessages(nextMessages);
    setInput("");
    setLoading(true);

    const response = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages: nextMessages })
    });

    const data = await response.json();

    setMessages([
      ...nextMessages,
      { role: "assistant", content: data.content }
    ]);
    setLoading(false);
  }

  return (
    <main>
      <section>
        {messages.map((message, index) => (
          <div key={index}>
            <strong>{message.role === "user" ? "你" : "AI"}</strong>
            <p>{message.content}</p>
          </div>
        ))}
      </section>

      <textarea
        value={input}
        onChange={(event) => setInput(event.target.value)}
      />
      <button onClick={sendMessage} disabled={loading}>
        {loading ? "生成中" : "发送"}
      </button>
    </main>
  );
}

页面接入

src/app/page.tsx 中使用:

import { ChatPanel } from "@/components/chat/ChatPanel";

export default function Page() {
  return <ChatPanel />;
}

现在还缺什么

这个 Chat 已经能工作,但体验很粗糙:

  • 模型返回前,用户只能等待
  • 长回答不会逐字显示
  • 没有错误提示
  • 没有取消生成
  • 没有引用来源
  • 没有工具调用状态

这些问题会在后续章节逐步解决。

实战任务

完成:

  • /api/chat 接口
  • ChatPanel 组件
  • 基础消息列表
  • 输入框和发送按钮
  • loading 状态

常见坑

不要把所有历史消息无限传给模型。短期测试可以这么做,生产环境要做截断、摘要或持久化。

不要只在前端做 loading。后端也要处理超时和异常。

不要在组件里拼 system prompt。system prompt 应该放在服务端。

本章小结

我们完成了最小 Chat。下一章会把一次性返回改造成流式输出,让体验接近 ChatGPT。