第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 实验平台搭建
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!