LangChain.js 完全开发手册(十三)AI Agent 生态系统与工具集成

第13章:AI Agent 生态系统与工具集成

前言

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

🎯 本章学习目标

  • 体系化掌握 Agent 工具生态接入:搜索、日历、邮件、工单、文档、数据仓库等
  • 设计安全合规的工具适配层:鉴权与密钥管理、限流与配额、审计与回放、沙箱执行
  • 建立可扩展的 Tool Registry 与协议:Zod Schema、幂等设计、超时/重试/熔断
  • 用 LangGraph 编排多工具与多 Agent 协作:计划→并行执行→归并→冲突解决
  • 在 Next.js 中实现 OAuth 回调、工具授权页面、执行可视化时间线
  • 实战项目:企业协同助手(会议安排 + 纪要生成 + 工单创建 + 通知发送)

🧩 工具适配层设计(Adapter Pattern)

13.1 设计原则

  • 标准化:统一 name/description/schema/_call 接口,与 LangChain Tool 对齐
  • 安全性:最小权限(OAuth scope)、租户隔离、速率限制、审计留痕
  • 稳定性:超时/重试/熔断/降级;幂等键(Idempotency-Key)
  • 可观测:结构化日志、指标打点、RunId 串联全链路
  • 可扩展:注册中心 + 插件化加载 + 版本管理(v1/v2)

13.2 基础接口与注册中心

typescript 复制代码
// 文件:src/ch13/tooling/core.ts
import { Tool } from "@langchain/core/tools";
import { z } from "zod";

export interface ToolContext {
  userId: string;
  tenantId: string;
  auth?: Record<string, any>; // 已授权的凭据/令牌
  runId?: string;
}

export abstract class SafeTool<T extends z.ZodTypeAny> extends Tool {
  schema!: T;
  abstract _callWithContext(input: z.infer<T>, ctx: ToolContext): Promise<any>;

  async _call(input: any) { throw new Error("Use _callWithContext with ToolRunner"); }
}

export class ToolRegistry {
  private tools = new Map<string, SafeTool<any>>();
  register(t: SafeTool<any>) { this.tools.set(t.name, t); }
  get(name: string) { return this.tools.get(name); }
  list() { return [...this.tools.values()]; }
}

export class ToolRunner {
  constructor(private registry: ToolRegistry) {}
  async run(name: string, args: any, ctx: ToolContext) {
    const tool = this.registry.get(name);
    if (!tool) throw new Error(`工具不存在: ${name}`);
    // 基础策略:超时、重试、速率限制、审计
    return withTimeout(() => withRetry(() => tool._callWithContext(args, ctx), 2), 15_000);
  }
}

export async function withRetry<T>(fn: () => Promise<T>, tries = 2) {
  let last: any; for (let i=0;i<tries;i++){ try{ return await fn(); }catch(e){ last=e; await sleep(200*(i+1)); }}
  throw last;
}
export async function withTimeout<T>(fn: () => Promise<T>, ms: number) {
  let t: any; return Promise.race([fn(), new Promise((_,rej)=> t=setTimeout(()=>rej(new Error("timeout")), ms))]).finally(()=>clearTimeout(t));
}
export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));

🔐 授权与密钥管理(OAuth2/OIDC + KMS)

13.3 凭据模型

typescript 复制代码
// 文件:src/ch13/auth/creds.ts
export type Credential = {
  provider: 'google'|'slack'|'jira'|'notion'|'github'|'outlook';
  tenantId: string; userId: string; scope: string[];
  accessToken: string; refreshToken?: string; expiresAt?: number; meta?: any;
};

13.4 Next.js OAuth 回调(示意)

typescript 复制代码
// 文件:src/app/api/oauth/callback/route.ts
import { NextRequest } from "next/server";
export const runtime = "edge";

export async function GET(req: NextRequest) {
  const url = new URL(req.url);
  const provider = url.searchParams.get('provider');
  const code = url.searchParams.get('code');
  // 交换 token(省略具体实现),写入加密存储(KMS/Secrets Manager/DB KMS 列)
  // 重定向到工具授权成功页面
  return new Response(null, { status: 302, headers: { Location: `/tools?provider=${provider}&ok=1` } });
}

13.5 工具执行时提取凭据

typescript 复制代码
// 文件:src/ch13/auth/context.ts
import { ToolContext } from "../tooling/core";

export async function buildToolContext(userId: string, tenantId: string): Promise<ToolContext> {
  // 从安全存储读取用户已授权的第三方凭据
  const auth = { google: { accessToken: "..." }, slack: { accessToken: "..." } };
  return { userId, tenantId, auth };
}

🔌 常用企业工具适配实现

13.6 Google Calendar(日程)

typescript 复制代码
// 文件:src/ch13/tools/google-calendar.ts
import { z } from "zod";
import { SafeTool, ToolContext } from "../tooling/core";

export class GoogleCalendarTool extends SafeTool<typeof GoogleCalendarTool.schema> {
  name = "google_calendar";
  description = "在 Google Calendar 中创建会议事件";
  static schema = z.object({
    title: z.string(),
    start: z.string().describe("ISO 时间"),
    end: z.string().describe("ISO 时间"),
    attendees: z.array(z.string()).default([]),
    description: z.string().optional(),
  });
  schema = GoogleCalendarTool.schema;

  async _callWithContext(input: z.infer<typeof GoogleCalendarTool.schema>, ctx: ToolContext) {
    if (!ctx.auth?.google?.accessToken) throw new Error("未授权 Google");
    // 伪调用:实际应请求 Google Calendar API
    const eventId = `ev_${Date.now()}`;
    return { ok: true, eventId, link: `https://calendar.google.com/event?eid=${eventId}` };
  }
}

13.7 Slack(消息通知)

typescript 复制代码
// 文件:src/ch13/tools/slack.ts
import { z } from "zod";
import { SafeTool, ToolContext } from "../tooling/core";

export class SlackNotifyTool extends SafeTool<typeof SlackNotifyTool.schema> {
  name = "slack_notify";
  description = "在 Slack 频道或用户发送消息";
  static schema = z.object({ channel: z.string(), text: z.string() });
  schema = SlackNotifyTool.schema;
  async _callWithContext(input: z.infer<typeof SlackNotifyTool.schema>, ctx: ToolContext) {
    if (!ctx.auth?.slack?.accessToken) throw new Error("未授权 Slack");
    // 伪调用 Slack chat.postMessage
    return { ok: true, ts: Date.now(), channel: input.channel };
  }
}

13.8 Jira(工单)

typescript 复制代码
// 文件:src/ch13/tools/jira.ts
import { z } from "zod";
import { SafeTool, ToolContext } from "../tooling/core";

export class JiraIssueTool extends SafeTool<typeof JiraIssueTool.schema> {
  name = "jira_create_issue";
  description = "在 Jira 中创建工单(示例项目 KEY=PROJ)";
  static schema = z.object({
    summary: z.string(),
    description: z.string(),
    priority: z.enum(["Low","Medium","High"]).default("Medium"),
  });
  schema = JiraIssueTool.schema;
  async _callWithContext(input: z.infer<typeof JiraIssueTool.schema>, ctx: ToolContext) {
    // 伪调用 Jira REST API
    const key = `PROJ-${Math.floor(Math.random()*10000)}`;
    return { ok: true, key, url: `https://jira.example/browse/${key}` };
  }
}

13.9 Notion(知识卡片)

typescript 复制代码
// 文件:src/ch13/tools/notion.ts
import { z } from "zod";
import { SafeTool, ToolContext } from "../tooling/core";

export class NotionPageTool extends SafeTool<typeof NotionPageTool.schema> {
  name = "notion_create_page";
  description = "在 Notion 创建会议纪要/知识卡片";
  static schema = z.object({ title: z.string(), content: z.string() });
  schema = NotionPageTool.schema;
  async _callWithContext(input: z.infer<typeof NotionPageTool.schema>, ctx: ToolContext) {
    const pageId = `pg_${Date.now()}`;
    return { ok: true, pageId, url: `https://notion.so/${pageId}` };
  }
}

13.10 Web 搜索(代理)

typescript 复制代码
// 文件:src/ch13/tools/search.ts
import { z } from "zod"; import { SafeTool, ToolContext } from "../tooling/core";
export class WebSearchTool extends SafeTool<typeof WebSearchTool.schema> {
  name = "web_search"; description = "通用 Web 搜索(代理到自建/第三方搜索服务)";
  static schema = z.object({ query: z.string(), k: z.number().default(5) }); schema = WebSearchTool.schema;
  async _callWithContext(input: any, ctx: ToolContext) { return { results: Array.from({length: input.k}).map((_,i)=>({ title:`${input.query}-${i}`, url:`https://example.com/${i}`})) }; }
}

🧠 Agent × 工具:计划与执行

13.11 工具注册与执行策略

typescript 复制代码
// 文件:src/ch13/tooling/register.ts
import { ToolRegistry, ToolRunner } from "./core";
import { GoogleCalendarTool } from "../tools/google-calendar";
import { SlackNotifyTool } from "../tools/slack";
import { JiraIssueTool } from "../tools/jira";
import { NotionPageTool } from "../tools/notion";
import { WebSearchTool } from "../tools/search";

export function buildRegistry() {
  const reg = new ToolRegistry();
  [new GoogleCalendarTool(), new SlackNotifyTool(), new JiraIssueTool(), new NotionPageTool(), new WebSearchTool()].forEach(t=>reg.register(t));
  return { registry: reg, runner: new ToolRunner(reg) };
}

13.12 计划 → 并行执行 → 归并

typescript 复制代码
// 文件:src/ch13/agent/plan-exec.ts
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { buildRegistry } from "../tooling/register";
import { buildToolContext } from "../auth/context";

const planPrompt = PromptTemplate.fromTemplate(`
将目标分解为工具调用步骤(JSON): [{id, tool, args, description}]
目标: {goal}
可用工具: {tools}
输出: JSON 数组
`);

export async function runPlanned(goal: string, userId: string, tenantId: string) {
  const llm = new ChatOpenAI({ temperature: 0 });
  const { registry, runner } = buildRegistry();
  const toolsDesc = registry.list().map(t=>`${t.name}: ${t.description}`).join("\n");
  const planRes = await planPrompt.pipe(llm).invoke({ goal, tools: toolsDesc });
  let steps: any[]; try { steps = JSON.parse(String(planRes.content)); } catch { steps = []; }

  const ctx = await buildToolContext(userId, tenantId);
  const results = await Promise.allSettled(steps.map(s => runner.run(s.tool, s.args, ctx)));
  return steps.map((s, i) => ({ step: s, result: results[i].status === 'fulfilled' ? results[i].value : { error: (results[i] as any).reason?.message } }));
}

🌳 LangGraph 多工具协作(含冲突解决)

13.13 状态与节点

typescript 复制代码
// 文件:src/ch13/graph/state.ts
export type CoopState = {
  goal: string;
  plan?: any[];
  exec?: Array<{ step: any; result: any }>;
  merged?: any;
  conflict?: { reason: string; details: any };
  timeline: any[];
};
typescript 复制代码
// 文件:src/ch13/graph/nodes.ts
import { StateGraph } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { runPlanned } from "../agent/plan-exec";
import { CoopState } from "./state";

export async function planNode(s: CoopState): Promise<Partial<CoopState>> {
  const out = await runPlanned(`请完成:${s.goal}`, 'u1', 't1');
  return { exec: out, timeline: [{ type: 'exec', at: Date.now(), data: out }] };
}

const mergePrompt = PromptTemplate.fromTemplate(`
给定多工具结果,整合为最终结构:
输入: {results}
输出JSON: {"summary": string, "actions": string[]}
`);

export async function mergeNode(s: CoopState): Promise<Partial<CoopState>> {
  const llm = new ChatOpenAI({ temperature: 0.2 });
  const res = await mergePrompt.pipe(llm).invoke({ results: JSON.stringify(s.exec) });
  let merged: any; try { merged = JSON.parse(String(res.content)); } catch { merged = { summary: String(res.content) }; }
  return { merged, timeline: [{ type: 'merge', at: Date.now(), data: merged }] };
}

const conflictPrompt = PromptTemplate.fromTemplate(`
检查是否存在冲突(时间冲突/重复通知/权限不足),输出:{"conflict": boolean, "reason": string}
输入: {merged}
`);

export async function conflictNode(s: CoopState): Promise<Partial<CoopState>> {
  const llm = new ChatOpenAI({ temperature: 0 });
  const res = await conflictPrompt.pipe(llm).invoke({ merged: JSON.stringify(s.merged) });
  let j: any; try { j = JSON.parse(String(res.content)); } catch { j = { conflict: false }; }
  return j.conflict ? { conflict: { reason: j.reason, details: s.merged } } : { };
}

13.14 图与条件流

typescript 复制代码
// 文件:src/ch13/graph/app.ts
import { StateGraph } from "@langchain/langgraph";
import { CoopState } from "./state";
import { planNode, mergeNode, conflictNode } from "./nodes";

export async function buildCoopGraph() {
  const g = new StateGraph<CoopState>({ channels: {
    goal: { value: "" }, plan: { value: [] }, exec: { value: [] }, merged: { value: {} }, conflict: { value: undefined }, timeline: { value: [], merge: (a,b)=>[...a,...b] }
  }});
  g.addNode('plan', planNode);
  g.addNode('merge', mergeNode);
  g.addNode('conflict', conflictNode);
  g.addEdge('start','plan'); g.addEdge('plan','merge');
  g.addConditionalEdges('merge', s => s.merged ? 'conflict' : 'end', { conflict: 'conflict' });
  g.addConditionalEdges('conflict', s => s.conflict ? 'end' : 'end', {});
  return g.compile();
}

🌐 Next.js 集成:授权页、执行 API 与可视化

13.15 授权页(前端)

tsx 复制代码
// 文件:src/app/tools/page.tsx
"use client";
export default function ToolsPage() {
  const providers = [
    { id: 'google', name: 'Google Calendar' },
    { id: 'slack', name: 'Slack' },
    { id: 'jira', name: 'Jira' },
    { id: 'notion', name: 'Notion' },
  ];
  return (
    <main className="max-w-3xl mx-auto p-4 space-y-4">
      <h1 className="text-2xl font-bold">工具授权</h1>
      <ul className="space-y-2">
        {providers.map(p => (
          <li key={p.id} className="flex items-center justify-between border rounded p-3">
            <div>{p.name}</div>
            <a className="px-3 py-1 rounded bg-blue-600 text-white" href={`/api/oauth/callback?provider=${p.id}`}>授权</a>
          </li>
        ))}
      </ul>
    </main>
  );
}

13.16 执行 API

typescript 复制代码
// 文件:src/app/api/coop/route.ts
import { NextRequest } from "next/server";
import { buildCoopGraph } from "@/src/ch13/graph/app";
export const runtime = "edge";

export async function POST(req: NextRequest) {
  const { goal } = await req.json();
  const app = await buildCoopGraph();
  const out = await app.invoke({ goal, timeline: [] });
  return Response.json({ ok: true, data: out });
}

13.17 可视化页面

tsx 复制代码
// 文件:src/app/coop/page.tsx
"use client";
import { useState } from "react";

export default function CoopPage(){
  const [goal, setGoal] = useState("");
  const [data, setData] = useState<any>(null);
  const run = async ()=>{
    const res = await fetch('/api/coop', { method:'POST', body: JSON.stringify({ goal }) });
    const json = await res.json(); setData(json.data);
  };
  return (
    <main className="max-w-3xl mx-auto p-4 space-y-3">
      <h1 className="text-2xl font-bold">企业协同助手</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>
      {data && <pre className="whitespace-pre-wrap break-words text-sm bg-gray-50 p-3 rounded">{JSON.stringify(data, null, 2)}</pre>}
    </main>
  );
}

🚀 实战:企业协同助手(会议安排 + 纪要 + 工单 + 通知)

13.18 场景描述

  • 输入:自然语言目标,例如"安排周三下午30分钟评审会,参与 A/B/C;会后出纪要并将 action items 生成 Jira 工单;在 Slack 通知团队。"
  • 流程:计划 → 创建日历 → 生成纪要模版 → 创建工单 → 发送通知 → 回执
  • 守护:权限检查/时间冲突/重复通知/失败重试/人工确认

13.19 关键策略

  • 幂等:对同一 goal + time 生成相同 Idempotency-Key,避免重复
  • 安全:OAuth scope 最小化;工具参数白名单;对外链接检测
  • 可观测:每步计时、错误分类、结果回放(含外部链接和响应摘要)

13.20 示例执行(伪)

json 复制代码
{
  "goal": "安排周三评审会+纪要+工单+通知",
  "timeline": [
    {"type":"exec","at":1710000000,"data":[{"step":{"id":1,"tool":"google_calendar"},"result":{"ok":true,"eventId":"ev_..."}}]},
    {"type":"exec","at":1710000100,"data":[{"step":{"id":2,"tool":"notion_create_page"},"result":{"ok":true,"pageId":"pg_..."}}]},
    {"type":"exec","at":1710000200,"data":[{"step":{"id":3,"tool":"jira_create_issue"},"result":{"ok":true,"key":"PROJ-123"}}]},
    {"type":"exec","at":1710000300,"data":[{"step":{"id":4,"tool":"slack_notify"},"result":{"ok":true,"channel":"#team"}}]}
  ],
  "merged": {"summary":"会议已安排,纪要创建,工单生成,已通知"}
}

⚙️ 工程化:配额、限流、审计与沙箱

13.21 配额与限流

  • 维度:按租户/用户/工具/分钟/天;超限后软性降级或硬性拒绝
  • 共享额度:团队级池化;关键工具独立额度(如 Calendar)

13.22 审计与回放

  • 记录:输入、调用参数、响应摘要、外部链接、操作者与 RunId
  • 回放:敏感工具需双人复核模式(四眼原则)

13.23 沙箱与策略

  • 代码执行工具需强沙箱(见第8章),禁止文件/网络/系统调用
  • 生成类工具输出需正则/策略检查(敏感词/泄露检测)

🧪 测试与验收

13.24 工具级测试

  • 单元测试:参数校验、错误路径、重试与超时
  • 集成测试:OAuth 授权回调、令牌刷新、权限拒绝

13.25 端到端回归

  • 场景脚本:常见协同任务的黄金集;比对成功率、平均用时、失败分类
  • 灰度策略:新工具/新版本先小流量试运行,观测稳定后全量

📚 延伸链接

  • Google Calendar API:https://developers.google.com/calendar/api
  • Slack API:https://api.slack.com/
  • Jira REST API:https://developer.atlassian.com/cloud/jira/platform/rest/v3/
  • Notion API:https://developers.notion.com/
  • OAuth 2.0:https://oauth.net/2/

✅ 本章小结

  • 构建了安全可扩展的工具适配层与注册执行框架
  • 集成常用企业工具(日历/消息/工单/文档/搜索)并与 Agent 协作
  • 用 LangGraph 编排多工具结果并进行冲突检测与归并
  • 在 Next.js 中实现授权、执行与可视化,完成企业协同助手实战

🎯 下章预告

下一章《生产环境部署与 DevOps 实践》中,我们将:

  • 设计多环境部署与密钥管理
  • 构建 CI/CD 与灰度回滚
  • 打造生产级可观测与告警

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

相关推荐
汤面不加鱼丸2 分钟前
Vibe Coding初体验之Trae CN
ai编程
迷途酱10 分钟前
告别"玩具项目":用 MCP 协议让你的 AI Agent 真正干活
ai编程·mcp
卸任11 分钟前
Electron霸屏功能总结
前端·react.js·electron
fengci.11 分钟前
ctfshow黑盒测试前半部分
前端
喵个咪22 分钟前
Headless 架构优势:内容与展示解耦,一套 API 打通全端生态
前端·后端·cms
小江的记录本26 分钟前
【JEECG Boot】 JEECG Boot——数据字典管理 系统性知识体系全解析
java·前端·spring boot·后端·spring·spring cloud·mybatis
喵个咪29 分钟前
传统 CMS 太笨重?试试 Headless 架构的 GoWind,轻量又强大
前端·后端·cms
chenjingming66630 分钟前
jmeter导入浏览器上按F12抓的数据包
前端·chrome·jmeter
张元清30 分钟前
不用 Server Components 也能做 React 流式 SSR —— 实战指南
前端·javascript·面试