如何用AI 来做一个RAG 应用 (持续更新)

41 阅读7分钟

首先,明确一下需求 全栈,项目, 对话,基于RAG 来实现

在目前和ai的结对编程中, 虽然ai 逐步强大,但是作为开发者,始终要作为一个更上层的,调度和指挥,不能全部都相信ai ,要有自己的独立思考能力

明确需求

当时,公司内部,上网采用虚拟,机, 很多时候不是特别方便,内部之前开发的智能文档,更多的是基于模型本身的能力,而不是基于每个部门,公司内部的各种具体的代码规范 每次生成的业务组件,或者小模块的,案例,都是外部代码规范,我们公司内很多组件都是自研平台, 生成的代码无法,即可使用,效率低

目标,希望能给文档,或者要求,能结合公司内部的规范,直接生成可用 规范代码 不做模块,项目级的生成.

技术选项

因为目前是希望,能快速落地一个,以下都会配合ai来进行处理

我初始本身不是全栈项目,

这里借助了Tera 来进行构建 初始的 Next.js 运用

prompt

首先我们来借助 ai生成一个专业的资深前端 promot

# Role: Senior Frontend Engineer & Tech Lead

## Profile
你是一位拥有 10+ 年经验的资深前端工程师(Tech Lead 级别)。你不仅精通代码实现,更深谙软件架构、性能优化、用户体验(UX/UI)以及工程化最佳实践。你对代码质量有洁癖,拒绝“能跑就行”的低质量代码。

## Core Competencies
- **Frameworks**: Deep mastery of React (Hooks, Context, Patterns), Vue 3, or Angular (adapt to user request).
- **Language**: TypeScript Wizard (Strict Mode, Advanced Types, Generics).
- **Styling**: Tailwind CSS, CSS Modules, Styled Components, Sass.
- **Performance**: Core Web Vitals, Code Splitting, Lazy Loading, Memoization strategies.
- **Architecture**: Atomic Design, Clean Architecture, State Management (Zustand/Redux/Recoil).
- **Quality**: Jest, Cypress, Playwright, TDD, CI/CD pipelines.

## Coding Philosophy (The "Gold Standard")
在生成代码时,你必须严格遵守以下原则:
1.  **Type Safety**: 永远优先使用 TypeScript。拒绝 `any`,为 PropsStateAPI 响应定义清晰的接口。
2.  **Clean Code**:
    -   变量命名语义化(Self-documenting code)。
    -   遵循 DRY (Don't Repeat Yourself) 和 SOLID 原则。
    -   函数保持短小精悍,单一职责。
3.  **Performance First**: 警惕不必要的重渲染。默认考虑性能边界情况(如大数据量列表)。
4.  **Robustness**: 总是处理 Loading 状态、Error 状态和空数据状态(Empty States)。永远不要让 UI 崩溃。
5.  **Accessibility (a11y)**: 代码默认符合 WCAG 标准(语义化 HTML, ARIA 属性, 键盘导航)。

## Interaction Guidelines
1.  **Think Before You Code**: 在写代码前,先简要分析问题,提出技术方案或组件结构。
2.  **Explain "Why"**: 不要只给代码,要解释为什么这么写(例如:“这里使用 `useMemo` 是为了避免... ”)。
3.  **Challenge Bad Ideas**: 如果用户的需求会导致性能问题或反模式,礼貌地指出并提供更好的替代方案。
4.  **Incremental approach**: 对于复杂任务,分步骤实现,而不是一次性输出一坨巨大的代码块。

## Output Format
- **File Structure**: 明确指出建议的文件路径(例如:`src/components/Button/Button.tsx`)。
- **Code Blocks**: 使用标准 Markdown 代码块,带语言标签。
- **Comments**: 关键逻辑必须包含简短、有意义的注释。

---
**Instruction**: 现在,请等待我的具体需求。一旦我提出需求,请以资深专家的标准进行分析和编码。

技术设计,解耦

**Instruction**: 现在,请等待我的具体需求。一旦我提出需求,请以资深专家的标准进行分析和编码。

你先结合我的 目标 希望能给文档,或者要求,能结合公司内部的规范,直接生成可用 规范代码

不做模块,项目级的生成. 生成一个RAG 前端项目,

目前按照阶段一开发,目标,先为我划分好项目模块,先不添加任何 模版代码

要求,ai ,服务,业务逻辑,ui视图层解耦

image.png

模块分层/阶段的目标

你是一位资深前端架构师,请协助我设计一个 AI 对话式 Web 应用的前端架构。项目目标是:快速构建一个稳定、可交互、可演示的 MVP 前端界面,暂不依赖复杂后端逻辑,重点验证核心交互流程与扩展性设计。 #### 🎯 核心目标(MVP) - 用户能在浏览器中与 AI 进行多轮对话 - 界面需包含完整对话历史、消息流、输入控制 - 所有状态由前端管理(可 mock API),确保离线可演示 - 代码结构清晰,便于后续接入真实 AI 服务、多模型、多模态等能力 #### 🧩 模块划分要求(请按以下模块进行职责定义) 1. 对话列表侧边栏(Conversation List) - 展示用户已创建的对话会话(可本地存储) - 支持新建、切换、删除会话 - 初始可 mock 静态数据 2. 主对话窗口(Chat Panel) - 按时间顺序展示消息流 - 区分“用户消息”与“AI 消息”样式 - AI 消息需支持: - 加载状态(streaming 模拟) - 错误状态(如网络失败) - 重试按钮(点击后重新发送上一条用户消息) 3. 消息输入区(Input Area) - 支持文本输入(textarea) - 发送按钮 + 快捷键(Enter 发送) - 发送过程中: - 显示“暂停/取消”按钮(模拟中断 streaming) - 禁用输入框,防止重复提交 - 设计需考虑未来扩展: - 多模态输入(如上传图片、文件) - 多模型选择(下拉切换 GPT-4 / Claude / 本地模型等) #### ⚙️ 技术约束与原则 - 使用 React + TypeScript + Vite(默认技术栈) - 状态管理:优先使用 useReducer + useContextZustand(避免 Redux 重型方案) - 所有 AI 调用通过 mock service 实现(返回延迟 + 模拟流式响应) - 组件需 类型安全可测试无副作用耦合 - 目录结构需体现关注点分离(如 features/chat, entities/message, shared/ui) #### 📈 演进性要求 - 架构需支持未来轻松替换为真实 AI API(OpenAI / Ollama / 自研) - 输入区设计需预留 插槽(slots) 用于扩展多模态控件 - 消息渲染需支持 自定义渲染器(未来可渲染代码块、图片、表格等) 请基于以上要求: 1. 给出清晰的 模块职责划分图(文字描述) 2. 推荐 状态结构设计(如 message 对象字段) 3. 提供 关键组件接口定义(TypeScript interface) 4. 建议 目录结构 5. 指出 MVP 可省略但需预留扩展点的功能

能先有一个小的闭环场景

image.png

那现在我对于RAG 不是特别熟悉,那我就先引导 ai 来一步步拆解这个工程需求

那么技术选项 先用 常见的lanchin.js 来做项目

那我想先本地通过大模型,模拟对文本进行向量化,然后进行存储,存储完毕,后,我要进行本地验证,当前的逻辑是否可行

image.png

image.png

虽然我初始不理解整体的RAG 逻辑,但是借助ai ,提升,编程,已经做到了本地去验证

完善RAG 对话

思路

  1. 初始化 LLM
  2. 配合 Lanchin 链式模式,将用户输入的内容,和rag 文本库内容,进行统一调度
export const getLLm = async (query: string, onChunk: (chunk: string) => void) => {
  const llm = init();

  // Create the retrieval chain
  const chain = RunnableSequence.from([
    {
      context: async (input: string) => {
        const retrievedDocs = await search(input);
        const context = retrievedDocs.map(doc => doc.pageContent).join("\n\n");
        return context;
      },
      question: (input: string) => input,
    },
    ChatPromptTemplate.fromTemplate(`Answer the question based only on the following context:
{context}

Question: {question}`),
    llm,
    new StringOutputParser(),
  ]);

  const stream = await chain.stream(query);

  for await (const chunk of stream) {
    onChunk(chunk);
  }
};

如果加入历史记忆,可以去利用其存储,并解决

  const chatHistory = history.map(msg =>
    msg.role === 'user' ? new HumanMessage(msg.content) : new AIMessage(msg.content)
  );
 const chain = RunnableSequence.from([
    {
      context: async (input: string) => {
        let searchQuery = input;
        // Only rephrase if we have history
        if (chatHistory.length > 0) {
          try {
            console.log("[RAG] Rephrasing question with history...");
            // We need to pass the input manually to the rephrase chain
            searchQuery = await rephraseChain.invoke({
              chat_history: chatHistory,
              question: input
            });
            console.log(`[RAG] Rephrased Query: "${searchQuery}"`);
          } catch (e) {
            console.error("[RAG] Failed to rephrase question, using original:", e);
          }
        }

        const retrievedDocs = await search(searchQuery);
        const context = retrievedDocs.map(doc => doc.pageContent).join("\n\n");
        return context;
      },
      question: (input: string) => input,
      chat_history: () => chatHistory,
    },
    ChatPromptTemplate.fromMessages([
      ["system", `Answer the question based only on the following context:
{context}`],
      new MessagesPlaceholder("chat_history"),
      ["human", "{question}"],
    ]),
    llm,
    new StringOutputParser(),
  ]);

rag

一阶段

按照固定块,字符数,进行分组,但是只适用于简单文本 对于文档不适用

二阶段

通过语义,段落,代码块,进行分组,本质上就是通过

const smartSplitText = (text: string, maxTokens = 320, overlapTokens = 64) => {
  const lines = text.replace(/\r\n/g, "\n").split("\n");
  const segs: Array<{ type: "code" | "text"; content: string }> = [];
  let i = 0;
  let buf: string[] = [];
  while (i < lines.length) {
    const m = /^(```|~~~)\s*([a-zA-Z0-9+._-]*)?\s*$/.exec(lines[i]);
    if (m) {
      if (buf.length) {
        segs.push({ type: "text", content: buf.join("\n") });
        buf = [];
      }
      const marker = m[1];
      const start = i;
      i++;
      while (i < lines.length && !new RegExp(`^${marker}\\s*$`).test(lines[i])) i++;
      if (i < lines.length) i++;
      segs.push({ type: "code", content: lines.slice(start, i).join("\n") });
      continue;
    }
    buf.push(lines[i]);
    i++;
  }
  if (buf.length) segs.push({ type: "text", content: buf.join("\n") });
  const out: string[] = [];
  const pushWithOverlap = (chunk: string) => {
    if (out.length === 0) {
      out.push(chunk);
      return;
    }
    const prev = out[out.length - 1];
    const overlapChars = Math.max(0, Math.floor((overlapTokens * 4)));
    const tail = prev.slice(Math.max(0, prev.length - overlapChars));
    out.push(tail + (tail ? "\n" : "") + chunk);
  };
  for (const seg of segs) {
    if (seg.type === "code") {
      const t = estTokens(seg.content);
      if (t <= maxTokens) {
        pushWithOverlap(seg.content);
      } else {
        const codeLines = seg.content.split("\n");
        let buf2: string[] = [];
        for (const l of codeLines) {
          const tmp = buf2.length ? buf2.join("\n") + "\n" + l : l;
          if (estTokens(tmp) > maxTokens) {
            if (buf2.length) pushWithOverlap(buf2.join("\n"));
            buf2 = [l];
          } else {
            buf2.push(l);
          }
        }
        if (buf2.length) pushWithOverlap(buf2.join("\n"));
      }
    } else {
      const paras = seg.content.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
      let acc: string[] = [];
      for (const p of paras) {
        const tmp = acc.length ? acc.join("\n\n") + "\n\n" + p : p;
        if (estTokens(tmp) > maxTokens) {
          if (acc.length) pushWithOverlap(acc.join("\n\n"));
          acc = [p];
        } else {
          acc.push(p);
        }
      }
      if (acc.length) pushWithOverlap(acc.join("\n\n"));
    }
  }
  return out;
};

通过了对文档的段落的向量化,最终并记录原始文档行,号,从而展示出原始文件的链接

image.png

工程规范

后期规划