LangChain.js 完全开发手册(十)RAG 与 Agent 的协同编排实践

364 阅读5分钟

第10章:RAG 与 Agent 的协同编排实践

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!

🎯 本章学习目标

  • 统一 RAG 与 Agent 的协作范式:工具化检索、结构化回答、可追溯与可控
  • 构建“会计划、会检索、会调用外部 API、会自我修复”的混合型 Agent
  • 使用 LangGraph 编排 RAG 工具、计划分解、回退/重试、人工确认等复杂流程
  • 在 Next.js 中实现端到端 API(SSE)和前端可视化(步骤时间线 + 引用)
  • 实战项目:企业知识问答 + 工单自动化 + 数据对齐校验(双模态/多来源)
  • 工程化:缓存、降级、审计、安全与隐私隔离、基线评估与 A/B

🧩 协同范式:RAG 作为 Agent 的一等公民工具

10.1 为什么要将 RAG 变成“工具”

  • Agent 的核心是“根据目标与上下文选择合适的工具执行”,RAG 可被抽象为一种高质量的“知识检索工具”
  • 将“检索→融合→生成→引用校验”封装为一个受控的可观测工具,Agent 只需要传参与消费结果
  • 优点:可控性强、可替换(不同检索策略/向量库)、可评测(Recall/Citation)、易集成(和其它工具并列)

10.2 抽象接口设计

// 文件:src/ch10/rag-tool.ts
import { Tool } from "@langchain/core/tools";
import { z } from "zod";
import { buildRagPipeline } from "@/src/ch07/rag-pipeline";

export class RAGTool extends Tool {
  name = "rag_query";
  description = "基于企业知识库的检索增强问答,返回带引用与置信度的结构化结果";
  schema = z.object({
    question: z.string().describe("用户问题"),
    domain: z.string().default("news").describe("业务域,如 news/policy")
  });

  async _call(input: { question: string; domain?: string }) {
    const pipeline = await buildRagPipeline(input.domain || "news");
    const out = await pipeline.invoke(input.question);
    return JSON.stringify(out);
  }
}

🤖 计划驱动的混合型 Agent(Plan → Act → Observe → Reflect)

10.3 Agent 能力矩阵

  • RAG 检索(知识)
  • Calculator(数值计算)
  • Web/API(外部数据)
  • Time(时间/时区)
  • Ticket(工单/任务管理)
  • Reflect(自我反思与修正)

10.4 工具注册中心

// 文件:src/ch10/tools.ts
import { Tool } from "@langchain/core/tools";
import { SearchTool, CalculatorTool, TimeTool } from "@/src/ch08/tools";
import { RAGTool } from "./rag-tool";

export class ToolRegistry {
  private map = new Map<string, Tool>();
  constructor() {
    [new SearchTool(), new CalculatorTool(), new TimeTool(), new RAGTool()].forEach(t => this.map.set(t.name, t));
  }
  all() { return [...this.map.values()]; }
  get(name: string) { return this.map.get(name); }
}

10.5 计划分解 Agent(ReAct+Plan)

// 文件:src/ch10/plan-agent.ts
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { ToolRegistry } from "./tools";

export type PlanStep = { id: number; tool: string; args: any; goal: string };
export type Plan = { steps: PlanStep[]; rationale: string };

const planPrompt = PromptTemplate.fromTemplate(`
你是任务规划器。请将用户目标分解为若干可执行的步骤。
- 仅使用提供的工具。
- 工具参数必须是 JSON。
- 若需要企业知识请使用 rag_query。

可用工具:\n{tools}

用户目标:{goal}

输出 JSON:
{
  "rationale": string,
  "steps": [
    {"id": 1, "tool": string, "args": object, "goal": string},
    ...
  ]
}
`);

export class PlanAgent {
  private llm = new ChatOpenAI({ temperature: 0 });
  private registry = new ToolRegistry();

  async makePlan(goal: string): Promise<Plan> {
    const toolsDesc = this.registry.all().map(t => `${t.name}: ${t.description}`).join("\n");
    const res = await planPrompt.pipe(this.llm).invoke({ tools: toolsDesc, goal });
    const json = JSON.parse(String(res.content));
    return json as Plan;
  }
}

10.6 执行器:带观察与回写

// 文件:src/ch10/executor.ts
import { ToolRegistry } from "./tools";

export type ExecutionLog = {
  stepId: number;
  tool: string;
  args: any;
  observation: string;
  success: boolean;
  startedAt: number;
  endedAt: number;
  error?: string;
};

export class Executor {
  private registry = new ToolRegistry();

  async run(plan: { steps: any[] }): Promise<{ logs: ExecutionLog[] }> {
    const logs: ExecutionLog[] = [];
    for (const s of plan.steps) {
      const startedAt = Date.now();
      try {
        const tool = this.registry.get(s.tool);
        if (!tool) throw new Error(`工具不存在:${s.tool}`);
        const observation = await tool._call(s.args);
        logs.push({ stepId: s.id, tool: s.tool, args: s.args, observation, success: true, startedAt, endedAt: Date.now() });
      } catch (e: any) {
        logs.push({ stepId: s.id, tool: s.tool, args: s.args, observation: "", success: false, startedAt, endedAt: Date.now(), error: e.message });
        break;
      }
    }
    return { logs };
  }
}

10.7 反思与修正(Reflect)

// 文件:src/ch10/reflect.ts
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";

const reflectPrompt = PromptTemplate.fromTemplate(`
你是执行审计器。给定计划与执行日志,判断是否需要修正:
- 如果 rag_query 缺少引用或置信度过低,建议增大 TopK 或改写查询
- 如果外部 API 失败,建议重试或降级

输入:
计划:{plan}
日志:{logs}
输出 JSON:
{"need_fix": boolean, "new_plan": object | null, "notes": string}
`);

export async function reflect(plan: any, logs: any) {
  const llm = new ChatOpenAI({ temperature: 0 });
  const res = await reflectPrompt.pipe(llm).invoke({ plan: JSON.stringify(plan), logs: JSON.stringify(logs) });
  return JSON.parse(String(res.content));
}

🌳 用 LangGraph 编排混合流程(Plan → Execute → Reflect → Loop)

10.8 状态定义

// 文件:src/ch10/state.ts
export type MixState = {
  goal: string;
  plan?: any;
  exec?: { logs: any[] };
  reflect?: { need_fix: boolean; new_plan?: any; notes?: string };
  result?: any;
  error?: string;
  timeline: any[];
};

10.9 节点实现

// 文件:src/ch10/nodes.ts
import { PlanAgent } from "./plan-agent";
import { Executor } from "./executor";
import { reflect } from "./reflect";
import { MixState } from "./state";

export async function planNode(s: MixState): Promise<Partial<MixState>> {
  const agent = new PlanAgent();
  const plan = await agent.makePlan(s.goal);
  return { plan, timeline: [{ type: "plan", at: Date.now(), data: plan }] };
}

export async function executeNode(s: MixState): Promise<Partial<MixState>> {
  const ex = new Executor();
  const exec = await ex.run(s.plan);
  return { exec, timeline: [{ type: "exec", at: Date.now(), data: exec }] };
}

export async function reflectNode(s: MixState): Promise<Partial<MixState>> {
  const rf = await reflect(s.plan, s.exec);
  return { reflect: rf, timeline: [{ type: "reflect", at: Date.now(), data: rf }] };
}

export async function finalizeNode(s: MixState): Promise<Partial<MixState>> {
  // 从执行日志中抽取最终结果(例如 rag_query 的回答)
  const last = [...(s.exec?.logs || [])].reverse().find(l => l.tool === "rag_query" && l.success);
  const result = last ? JSON.parse(last.observation) : { answer: "未得到有效结果" };
  return { result, timeline: [{ type: "final", at: Date.now(), data: result }] };
}

10.10 图与循环编排

// 文件:src/ch10/graph.ts
import { StateGraph } from "@langchain/langgraph";
import { MixState } from "./state";
import { planNode, executeNode, reflectNode, finalizeNode } from "./nodes";

export async function buildMixGraph() {
  const g = new StateGraph<MixState>({
    channels: {
      goal: { value: "" },
      plan: { value: {} },
      exec: { value: { logs: [] } },
      reflect: { value: { need_fix: false } },
      result: { value: {} },
      error: { value: "" },
      timeline: { value: [], merge: (a,b)=>[...a,...b] },
    }
  });

  g.addNode("plan", planNode);
  g.addNode("execute", executeNode);
  g.addNode("reflect", reflectNode);
  g.addNode("final", finalizeNode);

  g.addEdge("start", "plan");
  g.addEdge("plan", "execute");
  g.addEdge("execute", "reflect");

  // 若需要修正则回到 plan,否则进入最终
  g.addConditionalEdges("reflect", (s) => (s.reflect?.need_fix ? "replan" : "final"), {
    replan: "plan",
    final: "final",
  });

  g.addEdge("final", "end");

  return g.compile();
}

🌐 Next.js:SSE 实时时间线 + 引用展示

10.11 API:/api/mix(SSE)

// 文件:src/app/api/mix/route.ts
import { NextRequest } from "next/server";
import { buildMixGraph } from "@/src/ch10/graph";

export const runtime = "edge";

export async function POST(req: NextRequest) {
  const { goal } = await req.json();
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      try {
        const app = await buildMixGraph();
        controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "start" })}\n\n`));
        const out = await app.invoke({ goal, timeline: [] });
        for (const e of out.timeline) {
          controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "event", data: e })}\n\n`));
        }
        controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "result", data: out.result })}\n\n`));
      } catch (e: any) {
        controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "error", message: e.message })}\n\n`));
      } finally { controller.close(); }
    }
  });
  return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" } });
}

10.12 前端页面:步骤时间线 + 引用卡片

// 文件:src/app/mix/page.tsx
"use client";
import { useEffect, useRef, useState } from "react";

type EventItem = { type: string; at?: number; data: any };

export default function MixPage() {
  const [events, setEvents] = useState<EventItem[]>([]);
  const [result, setResult] = useState<any>(null);
  const [goal, setGoal] = useState("");
  const esRef = useRef<EventSource | null>(null);

  const run = async () => {
    setEvents([]); setResult(null);
    const res = await fetch("/api/mix", { method: "POST", body: JSON.stringify({ goal }) });
    // 简化:演示使用同一路由事件流
    const es = new EventSource("/api/mix");
    esRef.current = es;
    es.onmessage = (e) => {
      const msg = JSON.parse(e.data);
      if (msg.type === "event") setEvents(prev => [...prev, msg.data]);
      if (msg.type === "result") setResult(msg.data);
    };
  };

  return (
    <main className="max-w-4xl mx-auto p-4 space-y-4">
      <h1 className="text-2xl font-bold">RAG × Agent 协同编排</h1>
      <div className="flex gap-2">
        <input className="flex-1 border rounded px-3 py-2" value={goal} onChange={e=>setGoal(e.target.value)} placeholder="例如:给我一份本周行业新闻摘要并附引用" />
        <button onClick={run} className="px-4 py-2 bg-blue-600 text-white rounded">运行</button>
      </div>

      <section className="space-y-2">
        <h2 className="font-semibold">执行时间线</h2>
        <ol className="space-y-2">
          {events.map((e, idx) => (
            <li key={idx} className="border rounded p-3 bg-gray-50">
              <div className="text-sm opacity-70">{new Date(e.at || Date.now()).toLocaleString()}</div>
              <div className="font-medium">{e.type}</div>
              <pre className="whitespace-pre-wrap break-words text-xs">{JSON.stringify(e.data, null, 2)}</pre>
            </li>
          ))}
        </ol>
      </section>

      {result && (
        <section className="space-y-2">
          <h2 className="font-semibold">最终结果</h2>
          <pre className="whitespace-pre-wrap break-words text-sm bg-gray-50 p-3 rounded">{JSON.stringify(result, null, 2)}</pre>
          {Array.isArray(result.citations) && (
            <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
              {result.citations.map((c: any, i: number) => (
                <div key={i} className="border rounded p-3">
                  <div className="text-sm">来源:{c.source}</div>
                  <div className="text-xs opacity-70">片段:{c.chunkId}</div>
                </div>
              ))}
            </div>
          )}
        </section>
      )}
    </main>
  );
}

🚀 实战:企业知识问答 + 工单自动化 + 多来源对齐校验

10.13 需求

  • 用户发问 → 规划 → RAG 回答 → 若置信度低触发“备用来源(外部 API/搜索)”
  • 引用必需;多来源一致性校验(若冲突,触发人工确认或加权投票)
  • 对于复杂问题自动创建工单,并附上引用与检索片段作为证据

10.14 一致性校验器(示意)

// 文件:src/ch10/consistency.ts
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";

const prompt = PromptTemplate.fromTemplate(`
给定多个来源的答案与引用,判断是否一致,输出 JSON:
{"consistent": boolean, "reason": string, "decision": "trust_internal|trust_external|need_human"}

内部答案:{internal}
外部答案:{external}
引用:{cites}
`);

export async function checkConsistency(internal: any, external: any) {
  const llm = new ChatOpenAI({ temperature: 0 });
  const res = await prompt.pipe(llm).invoke({ internal: JSON.stringify(internal), external: JSON.stringify(external), cites: JSON.stringify([internal.citations, external.citations]) });
  return JSON.parse(String(res.content));
}

10.15 工单创建工具(示意)

// 文件:src/ch10/ticket.ts
import { Tool } from "@langchain/core/tools";
import { z } from "zod";

export class TicketTool extends Tool {
  name = "create_ticket";
  description = "创建工单,参数包含标题/描述/引用/优先级";
  schema = z.object({
    title: z.string(),
    description: z.string(),
    citations: z.array(z.object({ source: z.string(), chunkId: z.string() })).default([]),
    priority: z.enum(["low","medium","high"]).default("medium")
  });
  async _call(input: any) {
    const id = `T-${Date.now()}`;
    return `工单已创建:${id}`;
  }
}

10.16 组合 Plan:优先用 RAG,置信度低再搜索,冲突则审批

// 文件:src/ch10/policy-plan.ts
export const policyPlan = {
  rationale: "先用企业知识库;若低于0.6,再进行外部搜索;不一致则人工审批",
  steps: [
    { id: 1, tool: "rag_query", args: { question: "{goal}" }, goal: "企业知识回答" },
    { id: 2, tool: "search", args: { query: "{goal}" }, goal: "外部信息对齐" },
  ]
};

⚙️ 工程化:缓存、降级、审计、安全

10.17 缓存策略

  • 计划缓存:hash(goal) 作为 key,短期复用
  • RAG 结果缓存:对热门问题设置短期缓存,含引用与置信度
  • 工具级缓存与批处理(参见第5章)

10.18 降级与回退

  • RAG 失败 → 回退到 keyword + rerank
  • 外部 API 超时 → 返回内部答案并提示“外部数据暂不可用”
  • 图级别:超出最大循环次数直接产出“当前不可回答”的结构化卡片

10.19 审计与安全

  • 全链路审计:计划、工具调用、结果、引用、人工介入
  • 按租户/部门隔离知识库与权限
  • 提示注入防护:系统提示固定,工具参数白名单与正则校验

10.20 评估与 A/B

  • 基线:无外部搜索 vs 混合搜索;不同 TopK/MMR;不同反思策略
  • 指标:满意度、Citation Accuracy、平均延迟、成本、人工接管率

📚 延伸链接

  • LangChain Tools:https://js.langchain.com/docs/modules/tools/
  • LangGraph 状态图:https://langchain-ai.github.io/langgraph/
  • ReAct + RAG 实践:https://js.langchain.com/docs/use_cases/question_answering/

✅ 本章小结

  • 将 RAG 工具化,使其成为 Agent 的一等公民,提升可控与可观测
  • 构建了 Plan→Execute→Reflect 循环,支持修正、降级与人工审批
  • 用 LangGraph 编排复杂协同流程,并提供 Next.js 端到端实现
  • 给出了企业知识 + 工单自动化 + 一致性校验的实战路线

🎯 下章预告

下一章《评测与观测:让 AI 应用可度量可优化》中,我们将:

  • 构建评测集与 Golden Set
  • 指标体系:Recall/MRR/Citation/满意度/成本/延迟
  • LangSmith 集成与 A/B 实验平台搭建

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!