最近买了 《LangGraph 实战》这本书,有点学废了,尝试跟着官方文档重新学习了下 LangChain。

最近 LangChain LangGraph 都发布了 1.0 版本,他们也获得了大额融资,应该会有一定的稳定性。openai 之类本身的请求都非常简单,核心是 /completions,但是衍生的概念非常多,比如 human-in-loop,agentmemory。目前我们大部分的应用简单接口就能满足,后续,准备构建更加复杂的应用。

LangChain 现在是基于 LangGraph 构建的。LangChain 底层是 LangGraph。LangChain 一般用于线性场景,应用比较简单。LangGraph 基于图的概念构建,较为复杂,有多种状态以及概念。

本篇,贴一些我测试过的 demo ,给自己学习做个记录。

import { HumanMessage, SystemMessage } from "langchain";
import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";
const model = new ChatAlibabaTongyi({
    model: "qwen-plus",
    alibabaApiKey: "",
});
const systemMsg = new SystemMessage("You are a helpful assistant.");
const humanMsg = new HumanMessage("你是谁啊?");
const messages = [systemMsg, humanMsg];
const response = await model.invoke(messages); // Returns AIMessage
console.log(response.content);
// 使用自定义 BooleanOutputParser 示例
import { HumanMessage, SystemMessage } from "langchain";
import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";
import { BooleanOutputParser } from "./BooleanOutputParser";

// 创建布尔解析器
const booleanParser = new BooleanOutputParser();

// 创建模型链
const model = new ChatAlibabaTongyi({
  model: "qwen-plus",
  alibabaApiKey: "",
}).pipe(booleanParser);

// 示例 1:判断问题
const systemMsg1 = new SystemMessage(
  `你是一个助手。${booleanParser.getFormatInstructions()}`
);
const humanMsg1 = new HumanMessage("1 + 1 是否等于 2?");

console.log("问题 1: 1 + 1 是否等于 2?");
const response1 = await model.invoke([systemMsg1, humanMsg1]);
console.log("回答:", response1);  // 应该输出: true
console.log("类型:", typeof response1);  // 输出: boolean
console.log("---");

// 示例 2:判断问题
const systemMsg2 = new SystemMessage(
  `你是一个助手。${booleanParser.getFormatInstructions()}`
);
const humanMsg2 = new HumanMessage("地球是平的吗?");

console.log("问题 2: 地球是平的吗?");
const response2 = await model.invoke([systemMsg2, humanMsg2]);
console.log("回答:", response2);  // 应该输出: false
console.log("类型:", typeof response2);  // 输出: boolean
// 使用 StringOutputParser + 手动转换的简单方案
import { HumanMessage, SystemMessage } from "langchain";
import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";
import { StringOutputParser } from "@langchain/core/output_parsers";

// 创建模型链(使用 StringOutputParser)
const model = new ChatAlibabaTongyi({
  model: "qwen-plus",
  alibabaApiKey: "",
}).pipe(new StringOutputParser());

// 辅助函数:将字符串转换为布尔值
function parseBoolean(text: string): boolean {
  const trimmed = text.trim().toLowerCase();
  
  const trueValues = ["yes", "true", "1", "是", "对", "正确"];
  const falseValues = ["no", "false", "0", "否", "错", "错误"];
  
  if (trueValues.includes(trimmed)) {
    return true;
  }
  
  if (falseValues.includes(trimmed)) {
    return false;
  }
  
  throw new Error(`无法解析 "${text}" 为布尔值`);
}

// 示例使用
const systemMsg = new SystemMessage(
  "你是一个助手。请只用 yes 或 no 来回答问题。"
);
const humanMsg = new HumanMessage("1 + 1 是否等于 2?");

const response = await model.invoke([systemMsg, humanMsg]);
console.log("模型原始回答:", response);  // 字符串: "yes" 或 "no"

const booleanResult = parseBoolean(response);
console.log("解析后的布尔值:", booleanResult);  // 布尔值: true 或 false
console.log("类型:", typeof booleanResult);  // "boolean"
// 链使用
import { HumanMessage, SystemMessage } from "langchain";
import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";

// 修复输出解析器的导入路径
import { StringOutputParser } from "@langchain/core/output_parsers";

const model = new ChatAlibabaTongyi({
  model: "qwen-plus",
  alibabaApiKey: "",
})
// 这里使用了管道,就可以直接获取 content 内容了
.pipe(new StringOutputParser());



const systemMsg = new SystemMessage("You are a helpful assistant.");
const humanMsg = new HumanMessage("你是谁啊?");

const messages = [systemMsg, humanMsg];
const response = await model.invoke(messages);  // Returns string (因为使用了 StrOutputParser)

console.log(response);  // response 已经是字符串了,不需要 .content
// 复合链使用 - 新闻生成 + 结构化提取
// 注意,这里最终提取出了  JSON 结构的数据
import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";
import { StringOutputParser, StructuredOutputParser } from "@langchain/core/output_parsers";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableLambda } from "@langchain/core/runnables";

// 初始化模型
const model = new ChatAlibabaTongyi({
  model: "qwen-plus",
  alibabaApiKey: "",
});

// 第一步:根据标题生成新闻正文
const newsGenPrompt = ChatPromptTemplate.fromMessages([
  ["system", "请根据以下新闻标题撰写一段简短的新闻内容(100字以内):\n\n标题:{title}"],
]);

// 第一个子链:生成新闻内容
const newsChain = newsGenPrompt.pipe(model).pipe(new StringOutputParser());

// 方式1:普通函数(之前的写法)
const logNewsContent = (news: string) => {
  console.log("📰 生成的新闻内容:");
  console.log("─".repeat(50));
  console.log(news);
  console.log("─".repeat(50));
  console.log();
  return news; // 返回原内容,继续传递给下一个链
};

// 方式2:使用 RunnableLambda 包装(更规范的写法)
const logNewsContentRunnable = RunnableLambda.from((news: string) => {
  console.log("📰 生成的新闻内容(RunnableLambda版本):");
  console.log("─".repeat(50));
  console.log(news);
  console.log("─".repeat(50));
  console.log();
  return news;
});


// 第二步:从正文中提取结构化字段
const parser = StructuredOutputParser.fromNamesAndDescriptions({
  time: "事件发生的时间",
  location: "事件发生的地点",
  event: "发生的具体事件",
});

const summaryPrompt = ChatPromptTemplate.fromMessages([
  ["system", "请从下面这段新闻内容中提取关键信息,并返回结构化JSON格式:\n\n{news}\n\n{format_instructions}"],
]);

// 第二个子链:提取结构化信息
const summaryChain = summaryPrompt
  .pipe(model)
  .pipe(new StringOutputParser())
  .pipe(parser);

// 组合成一个复合 Chain(加入自定义组件)
// 可以选择使用普通函数或 RunnableLambda
const fullChain = newsChain
  .pipe(logNewsContentRunnable)  // 使用 RunnableLambda 版本(推荐)
  // .pipe(logNewsContent)  // 或者使用普通函数版本(也可以)
  .pipe((news) => {
    return summaryChain.invoke({
      news: news,
      format_instructions: parser.getFormatInstructions(),
    });
  });

// 调用复合链
console.log("开始执行复合链...\n");
const result = await fullChain.invoke({ title: "苹果公司在加州发布新款AI芯片" });
console.log("提取的结构化信息:");
console.log(JSON.stringify(result, null, 2));
// 多轮对话示例 - 流式输出版本
import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import * as readline from "readline";

// 初始化模型
const model = new ChatAlibabaTongyi({
  model: "qwen-plus",
  alibabaApiKey: "",
});

const parser = new StringOutputParser();

// 创建提示词模板(包含历史消息占位符)
const prompt = ChatPromptTemplate.fromMessages([
  ["system", "你叫成龙,是著名演员。"],
  new MessagesPlaceholder("messages"),  // 历史消息占位符
]);

// 创建链
const chain = prompt.pipe(model).pipe(parser);

// 初始化历史消息列表
const messagesList: (HumanMessage | AIMessage)[] = [];

// 创建命令行交互接口
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

console.log("🔹 多轮对话已开启(流式输出),输入 exit 或 quit 结束对话\n");

// 递归函数处理用户输入
const chat = () => {
  rl.question("你:", async (userQuery) => {
    // 检查是否退出
    if (userQuery.toLowerCase() === "exit" || userQuery.toLowerCase() === "quit") {
      console.log("\n👋 再见!");
      rl.close();
      return;
    }

    // 1) 追加用户消息
    messagesList.push(new HumanMessage(userQuery));

    try {
      // 2) 使用流式调用模型
      process.stdout.write("小智:");
      
      let fullResponse = "";
      const stream = await chain.stream({ messages: messagesList });
      
      // 逐块输出 流式输出
      for await (const chunk of stream) {
        process.stdout.write(chunk);  // 流式打印,不换行
        fullResponse += chunk;
      }
      
      console.log("\n");  // 输出完成后换行

      // 3) 追加 AI 回复到历史
      messagesList.push(new AIMessage(fullResponse));

      // 4) 仅保留最近 50 条消息(防止上下文过长)
      if (messagesList.length > 50) {
        messagesList.splice(0, messagesList.length - 50);
      }
    } catch (error) {
      console.error("\n❌ 错误:", error);
    }

    // 继续下一轮对话
    chat();
  });
};

// 开始对话
chat();
// LangChain 浏览器自动化 - TypeScript 版本
// 使用 Playwright 进行浏览器自动化操作
// 对应 Python 版本的 PlayWrightBrowserToolkit
// 注意:LangChain JS/TS 版本中没有内置的 PlaywrightBrowserToolkit
// 我们需要手动创建自定义工具来实现浏览器自动化功能

import { ChatOpenAI } from "@langchain/openai";
import { createAgent, tool } from "langchain";
import { chromium } from "playwright";
import { z } from "zod";

async function main() {
  // 1. 初始化 Playwright 浏览器
  const browser = await chromium.launch({
    headless: false, // 设置为 false 可以看到浏览器操作过程
  });

  const context = await browser.newContext();
  const page = await context.newPage();

  // 2. 创建自定义浏览器工具(模拟 Python 的 PlayWrightBrowserToolkit)

  // 工具1:导航到指定 URL
  const navigateTool = tool(
    async ({ url }: { url: string }) => {
      try {
        await page.goto(url, { waitUntil: "networkidle" });
        return `成功导航到: ${url}`;
      } catch (error: any) {
        return `导航失败: ${error.message}`;
      }
    },
    {
      name: "navigate",
      description: "导航到指定的 URL 地址",
      schema: z.object({
        url: z.string().describe("要访问的网页 URL"),
      }),
    }
  );

  // 工具2:获取页面内容
  const getPageContentTool = tool(
    async () => {
      try {
        // 提取文本内容
        const text = await page.evaluate(() => {
          // 移除 script 和 style 标签
          const scripts = document.querySelectorAll("script, style");
          scripts.forEach((el) => el.remove());
          return document.body.innerText;
        });
        return text.slice(0, 5000); // 限制返回内容长度
      } catch (error: any) {
        return `获取页面内容失败: ${error.message}`;
      }
    },
    {
      name: "get_page_content",
      description: "获取当前页面的文本内容",
      schema: z.object({}),
    }
  );

  // 工具3:点击元素
  const clickTool = tool(
    async ({ selector }: { selector: string }) => {
      try {
        await page.click(selector);
        return `成功点击元素: ${selector}`;
      } catch (error: any) {
        return `点击失败: ${error.message}`;
      }
    },
    {
      name: "click",
      description: "点击页面上的元素",
      schema: z.object({
        selector: z.string().describe("CSS 选择器"),
      }),
    }
  );

  // 工具4:填写表单
  const fillTool = tool(
    async ({ selector, text }: { selector: string; text: string }) => {
      try {
        await page.fill(selector, text);
        return `成功填写: ${selector}`;
      } catch (error: any) {
        return `填写失败: ${error.message}`;
      }
    },
    {
      name: "fill",
      description: "在输入框中填写文本",
      schema: z.object({
        selector: z.string().describe("CSS 选择器"),
        text: z.string().describe("要填写的文本"),
      }),
    }
  );

  // 工具5:截图
  const screenshotTool = tool(
    async ({ path }: { path: string }) => {
      try {
        await page.screenshot({ path });
        return `截图已保存到: ${path}`;
      } catch (error: any) {
        return `截图失败: ${error.message}`;
      }
    },
    {
      name: "screenshot",
      description: "对当前页面进行截图",
      schema: z.object({
        path: z.string().describe("截图保存路径"),
      }),
    }
  );

  // 工具列表
  const tools = [
    navigateTool,
    getPageContentTool,
    clickTool,
    fillTool,
    screenshotTool,
  ];

  // 3. 初始化模型(使用通义千问)
  const model = new ChatOpenAI({
    model: "qwen-plus",
    apiKey: "",
    configuration: {
      baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
    },
    temperature: 0,
  });

  // 4. 创建 Agent(对应 Python 的 create_openai_tools_agent)
  const agent = createAgent({
    model,
    tools,
    systemPrompt: `你是一个浏览器自动化助手。你可以使用以下工具来完成任务:
- navigate: 访问指定的网页
- get_page_content: 获取当前页面的文本内容
- click: 点击页面元素
- fill: 填写表单
- screenshot: 截图

请根据用户的指令,合理使用这些工具来完成任务。`,
  });

  try {
    // 5. 定义任务
    const command = {
      messages: [
        {
          role: "user",
          content:
            "访问这个网站 https://www.microsoft.com/en-us/microsoft-365/blog/2025/01/16/copilot-is-now-included-in-microsoft-365-personal-and-family/?culture=zh-cn&country=cn 并帮我总结一下这个网站的内容",
        },
      ],
    };

    // 6. 执行任务
    console.log("开始执行任务...");
    const response = await agent.invoke(command);
    console.log("\n执行结果:");
    console.log(response);
  } catch (error) {
    console.error("执行出错:", error);
  } finally {
    // 7. 清理资源
    await browser.close();
  }
}

// 执行主函数
main().catch(console.error);
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
import { createAgent } from "langchain";
import { ChatOpenAI } from "@langchain/openai";

const client = new MultiServerMCPClient({
  playwright: {
    command: "npx",
    args: ["@playwright/mcp@latest"],
    transport: "stdio",
  },
});

const model = new ChatOpenAI({
  model: "qwen-plus",
  apiKey: "",
  configuration: {
    baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
  },
  temperature: 0,
});

const tools = await client.getTools();
console.log("工具个数:", tools.length);
console.log("\n所有工具列表:");
tools.forEach((tool, index) => {
  console.log(`${index + 1}. ${tool.name} - ${tool.description}`);
});
const agent = createAgent({
  model,
  tools,
});



const response = await agent.invoke({
  messages: [{ role: "user", content: "打开这个页面,并总结其内容,https://jscoder.com/posts/20251019195842.html" }],
});

console.log(response);

// 清理资源并退出
await client.close();
process.exit(0);
import { StateGraph, START, END, Annotation } from "@langchain/langgraph";

// 定义 State 的类型注解
const StateAnnotation = Annotation.Root({
  x: Annotation<number>(),
});

// 创建一个 StateGraph
const builder = new StateGraph(StateAnnotation);

// 定义加法节点
function addition(state: typeof StateAnnotation.State) {
  console.log(`加法节点收到的初始值:`, state);
  return { x: state.x + 1 };
}

// 定义减法节点
function subtraction(state: typeof StateAnnotation.State) {
  console.log(`减法节点收到的初始值:`, state);
  return { x: state.x - 2 };
}

// 向图中添加两个节点
builder.addNode("addition", addition);
builder.addNode("subtraction", subtraction);

// 构建节点之间的边
builder.addEdge(START, "addition");
builder.addEdge("addition", "subtraction");
builder.addEdge("subtraction", END);

console.log("节点:", builder.nodes);
console.log("边:", builder.edges);

// 编译图
const graph = builder.compile();

// 打印图的 ASCII 表示
console.log(graph.getGraph().drawMermaid());

// 定义初始状态
const initialState = { x: 10 };

// 执行图
async function runGraph() {
  const result = await graph.invoke(initialState);
  console.log("最后的结果:", result);
}

runGraph();

import { StateGraph, Annotation, START } from "@langchain/langgraph";
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";

// 定义 State 类型,使用 Annotation 和 add_messages
const StateAnnotation = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    // add_messages 是一个 reducer,用于合并消息列表
    reducer: (left: BaseMessage[], right: BaseMessage[]) => {
      return left.concat(right);
    },
    default: () => [],
  }),
});

// 创建图构建器
const graph_builder = new StateGraph(StateAnnotation);

const model = new ChatOpenAI({
    model: "qwen-plus",
    apiKey: "",
    configuration: {
      baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
    },
    temperature: 0,
  });

// 定义 chatbot 节点函数
async function chatbot(
  state: typeof StateAnnotation.State
): Promise<Partial<typeof StateAnnotation.State>> {
  const response = await model.invoke(state.messages);
  return { messages: [response] };
}

// 添加节点
graph_builder.addNode("chatbot", chatbot);

// 添加边
graph_builder.addEdge(START, "chatbot");

// 编译图
const graph = graph_builder.compile();

// 测试多轮对话
async function runChatbot() {
  const messages_list: BaseMessage[] = [
    new HumanMessage("你好,我叫大模型真好玩,好久不见。"),
    new AIMessage("你好呀!我是苍老师,一名女演员。很高兴认识你!"),
    new HumanMessage("请问,你还记得我叫什么名字么?"),
  ];

  const final_state = await graph.invoke({ messages: messages_list });
  
  console.log("\n=== 对话历史 ===");
  final_state.messages.forEach((msg, index) => {
    const role = msg.type === "human" ? "用户" : "AI";
    console.log(`\n${index + 1}. [${role}]: ${msg.content}`);
  });
}

runChatbot();
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { BaseMessage, SystemMessage, HumanMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { z } from "zod";

// 定义 AgentState
const AgentStateAnnotation = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (left: BaseMessage[], right: BaseMessage[]) => {
      return left.concat(right);
    },
    default: () => [],
  }),
});

// 定义天气查询工具
const WeatherQuerySchema = z.object({
  loc: z.string().describe("城市名称"),
});

const get_weather = tool(
  async ({ loc }: { loc: string }) => {
    /**
     * 查询即时天气函数
     * @param loc - 必要参数,字符串类型,用于表示查询天气的具体城市名称
     * @returns 心知天气 API 查询即时天气的结果
     */
    const url = "https://api.seniverse.com/v3/weather/now.json";
    const params = new URLSearchParams({
      key:"", // 从环境变量读取
      location: loc,
      language: "zh-Hans",
      unit: "c",
    });

    try {
      const response = await fetch(`${url}?${params}`);
      const data = await response.json();
      return JSON.stringify(data.results[0].now);
    } catch (error) {
      return `查询天气失败: ${error}`;
    }
  },
  {
    name: "get_weather",
    description: "查询即时天气函数,用于获取指定城市的当前天气信息",
    schema: WeatherQuerySchema,
  }
);

// 初始化模型(使用通义千问)
const model = new ChatOpenAI({
  model: "qwen-plus",
  apiKey: "",
  configuration: {
    baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
  },
  temperature: 0,
});

// 绑定工具到模型
const tools = [get_weather];
const model_with_tools = model.bindTools(tools);

// 定义 call_model 节点
async function call_model(
  state: typeof AgentStateAnnotation.State
): Promise<Partial<typeof AgentStateAnnotation.State>> {
  const system_prompt = new SystemMessage(
    "你是一个AI助手,可以依据用户提问产生回答,你还具备调用天气函数的能力"
  );
  
  const response = await model_with_tools.invoke([system_prompt, ...state.messages]);
  return { messages: [response] };
}

// 创建工具节点
const tool_node = new ToolNode(tools);

// 定义条件判断函数
function should_continue(
  state: typeof AgentStateAnnotation.State
): "continue" | "end" {
  const messages = state.messages;
  const last_message = messages[messages.length - 1];
  console.log("last_message", last_message);
  // 检查最后一条消息是否包含工具调用
  if ("tool_calls" in last_message && last_message.tool_calls && last_message.tool_calls.length > 0) {
    return "continue";
  } else {
    return "end";
  }
}

// 构建图
const graph_builder = new StateGraph(AgentStateAnnotation);

// 添加节点
graph_builder.addNode("agent", call_model);
graph_builder.addNode("tools", tool_node);

// 添加边
graph_builder.addEdge(START, "agent");
graph_builder.addEdge("tools", "agent");

// 添加条件边
graph_builder.addConditionalEdges("agent", should_continue, {
  continue: "tools",
  end: END,
});

// 编译图
const graph = graph_builder.compile();

// 执行测试
async function runReACT() {
  console.log("=== ReACT 图执行开始 ===\n");
  
  const final_state = await graph.invoke({
    messages: [new HumanMessage("请问上海天气如何?")],
  });
  
  console.log("\n=== 完整对话历史 ===");
  final_state.messages.forEach((msg, index) => {
    console.log(`\n${index + 1}. [${msg._getType()}]:`);
    
    if (msg._getType() === "human") {
      console.log(`   内容: ${msg.content}`);
    } else if (msg._getType() === "ai") {
      console.log(`   内容: ${msg.content || "(调用工具)"}`);
      if ("tool_calls" in msg && msg.tool_calls && msg.tool_calls.length > 0) {
        console.log(`   工具调用:`, JSON.stringify(msg.tool_calls, null, 2));
      }
    } else if (msg._getType() === "tool") {
      console.log(`   工具返回: ${msg.content}`);
    }
  });
  
  console.log("\n=== ReACT 图执行结束 ===");
}

runReACT();