AI时代,使用大模型学习——LangChain (1)

62 阅读7分钟

前言

AI时代,人人都在用Cluade Code、Codex、Cusor等Agent开发,AI Agent最近已经变得非常强大,编译效率几乎比古法编程高了一个数量级。

在如此依赖AI Agent之后,我不禁产生一些疑问,我真的懂AI Agent吗?我真的会用AI Agent吗?在很多时候,AI Agent在我面前几乎是一个黑盒,我输入一些内容,然后AI Agent给我一些答复,写了一些代码,有时候这些代码都很正确,AI Agent似乎完全理解了我的需求,而有时候和我要的结果大相径庭。

几乎这些原因,我想我应该对AI Agent有一些更多的了解,实践是最好的老师,在这个想法之下,我问AI,我该如何学习开发自己的Agent,AI在思考了一会后输出了一大段文字,我在其中找到了一个适合入门的框架,LangChain。

用最好理解的话来说,LangChain就是一个用来开发Agent的框架,学习路劲比较平滑,有Python和TypeScript两种语言可选。

因为前端开发的背景,自然而然就选择了LangChain,有了学习的方向,该如何执行学习计划呢?没错,还是AI Agent,我新建了一个项目,然后和Cluade Code经过了简单的对话,我就得到了一个学习计划。然后再经过几轮对话,我得到了一个完整的项目,用于理解和学习LangChain

地址:gitcode.com/yuzhuohao/l…

基础概念

LLM(大语言模型)

大语言模型(Large Language Model,简称LLM)是一种基于深度学习的人工智能技术,主要用于生成和理解自然语言文本。这些模型通过在大量文本数据上进行训练,学习语言的模式和结构,从而能够生成自然语言文本或理解语言文本的含义。我们使用的ChatGpt、DeepSeek、Gemini等都是大语言模型。

一句话总结

LLM = 一个输入文字 → 输出文字的 AI 程序。你给它 Prompt,它返回 Completion。

Agent

Agent,或称智能代理,是指能够自主感知环境、做出决策并采取行动的计算实体。Agent的核心功能包括感知、决策和行动。这些功能使得Agent能够在动态环境中自主地执行任务,从而减少对人类干预的依赖。Agent 能自己做决策、多步执行、知道何时停止。Agent 把 LLM 放进反馈循环里,让它在"思考→行动→观察"中逼近答案。

一句话总结

Agent = LLM(大脑)+ Tools(手脚)+ 思考循环(什么时候做什么)。让 LLM 能自主决策、多步执行。

有了两个基础概念,让我们从代码来看Agent是如何实现的。

基本对话

要实现一个基本对话,只需要LLM的能力即可。

  // 1. 创建模型实例,使用deepseek
  const model = new ChatDeepSeek({
    model: config.model,
    apiKey: config.apiKey,
    temperature: 0.7,
  });

  // 2. 调用模型
  const result = await model.invoke("你好!请用一句话介绍你自己");

  // 3. 查看结果
  //    result 是 AIMessage 类型,包含多个字段
  console.log("✅ 收到回复:");
  console.log(`   ${result.content}\n`);

model.invoke() 是基础的大模型调用,类似使用deepSeek网页端进行一次对话,你问,大模型回答。

多轮对话

在人类的对话中,我们之所以能把聊天正常进行下去,是因为人类有记忆,能记住之前的对话。而大模型的每次对话都是独立的,如果没有上下文,大模型就只能进行单论的对话。

const model = new ChatDeepSeek({
    model: config.model,
    apiKey: config.apiKey,
    temperature: 0.7,
  });

  // ─── 演示 1:没有历史消息 ─────────────────
  console.log("📌 演示 1:没有上下文(模型不记得之前说了什么)\n");

  console.log("💬 Q1: 我叫小明,我是一名前端工程师");
  const r1 = await model.invoke("我叫小明,我是一名前端工程师");
  console.log(`🤖 A1: ${r1.content}`);

  console.log('\n💬 Q2: 我叫什么名字?我的职业是什么?');
  const r2 = await model.invoke("我叫什么名字?我的职业是什么?");
  console.log(`🤖 A2: ${r2.content}`);
  console.log("   ⚠️ 注意:模型不知道你是谁,因为每次 invoke 是独立的\n");

要让大模型具有上下文,需要用消息数组

  // ─── 演示 2:传递完整消息历史 ─────────────────
  console.log("📌 演示 2:传递完整消息历史(模型能记住上下文)\n");

  // 消息数组 = 对话状态
  // 类比前端的状态: const [messages, setMessages] = useState([])
  const messages: BaseMessage[] = [
    new SystemMessage("你是一个友好的助手,用中文回答问题。"),
    new HumanMessage("我叫小明,我是一名前端工程师"),
  ];

  // 第一轮
  const r3 = await model.invoke(messages);
  console.log(`💬 Q1: 我叫小明,我是一名前端工程师`);
  console.log(`🤖 A1: ${r3.content}`);

  // 把 AI 的回复也加入到消息历史中
  messages.push(r3);

  // 第二轮 —— 模型能记住!
  messages.push(new HumanMessage("我叫什么名字?我的职业是什么?"));
  console.log('\n💬 Q2: 我叫什么名字?我的职业是什么?');
  const r4 = await model.invoke(messages);
  console.log(`🤖 A2: ${r4.content}`);
  console.log("   ✅ 注意:这次模型记住了,因为历史消息一起传入了\n");

  console.log("📦 当前消息数组有", messages.length, "条消息:");
  messages.forEach((m, i) => {
    console.log(`   [${i}] ${m.type}: ${typeof m.content === 'string' ? m.content.substring(0, 40) + '...' : '[complex content]'}`);
  });

对话记忆

事实上,LangChain框架提供了封装的记忆功能,不需要手动在message中添加对话历史。

const model = new ChatDeepSeek({
    model: config.model,
    apiKey: config.apiKey,
    temperature: 0.7,
  });

  // 1. 创建对话历史管理器
  //    类比:这个就是对话的 "数据库"
  //    它负责存储和检索所有的对话消息
  const history = new InMemoryChatMessageHistory();

  console.log("📦 创建 InMemoryChatMessageHistory(内存对话历史)\n");

  // 2. 添加系统提示词
  await history.addMessage(
    new SystemMessage("你是一个友好的助手,用中文回答问题。")
  );

  // 3. 封装一个简单的聊天函数 —— 每次调用都自动:
  //    a) 获取历史消息
  //    b) 添加用户消息
  //    c) 调用模型
  //    d) 保存 AI 回复
  async function chat(userInput: string): Promise<string> {
    // 获取历史消息
    const pastMessages = await history.getMessages();

    // 添加新的用户消息并调用模型
    const messages = [...pastMessages, new HumanMessage(userInput)];
    const response = await model.invoke(messages);

    // 保存 AI 回复到历史
    await history.addMessage(new HumanMessage(userInput));
    await history.addMessage(response);

    return typeof response.content === "string" ? response.content : "[复杂内容]";
  }

  // 4. 多轮对话 —— 不需要手动管理 messages 数组!
  console.log("📌 演示:自动记忆对话\n");

  console.log("💬 用户: 我叫小明,我喜欢 React 和 TypeScript");
  const r1 = await chat("我叫小明,我喜欢 React 和 TypeScript");
  console.log(`🤖 AI: ${r1}\n`);

  console.log("💬 用户: 我最喜欢的后端框架是什么?");
  const r2 = await chat("我最喜欢的后端框架是什么?");
  console.log(`🤖 AI: ${r2}\n`);

  console.log("💬 用户: 我的名字是什么?我喜欢哪些技术?");
  const r3 = await chat("我的名字是什么?我喜欢哪些技术?");
  console.log(`🤖 AI: ${r3}\n`);

  // 5. 查看历史
  const allMessages = await history.getMessages();
  console.log(`📊 当前对话历史共 ${allMessages.length} 条消息\n`);

  // 6. 清空历史
  await history.clear();
  console.log("🧹 清空对话历史...");
  console.log(`📊 当前对话历史共 ${(await history.getMessages()).length} 条消息`);

封装的chat方法,会把每次对话的内容都存在历史中,让大模型拥有该次对话的记忆。

工具

Agent除了能进行对话之外,还有很重要的一个功能是可以使用工具,例如可以查询网络上的信息、修改本地文件等。

// ─── 1. 创建工具 ─────────────────
  // 工具 = 名字 + 描述 + 参数 schema + 执行函数
  // 类比前端的:定义一个带类型校验的 API handler

  const getWeather = tool(
    // 工具的执行函数 —— 真正干活的代码
    async ({ location }) => {
      // 模拟天气数据(实际项目中这里会调用真实的天气 API)
      const weatherData: Record<string, string> = {
        "北京": "晴天,25°C,湿度 40%,微风",
        "上海": "多云,28°C,湿度 65%,东南风 3 级",
        "深圳": "阵雨,30°C,湿度 80%,西南风 2 级",
      };
      return weatherData[location] ?? `未找到 ${location} 的天气数据`;
    },
    {
      // 工具元数据 —— 告诉 LLM 怎么调用这个工具
      name: "get_weather",
      description: "获取指定城市的当前天气信息。输入城市名称即可。",
      // Zod schema = 参数校验规则
      schema: z.object({
        location: z.string().describe("城市名称,例如:北京、上海、深圳"),
      }),
    }
  );

  const calculator = tool(
    async ({ expression }) => {
      try {
        // 注意:生产环境不要直接 eval,这里仅用于学习演示
        const result = Function(`"use strict"; return (${expression})`)();
        return `计算结果: ${expression} = ${result}`;
      } catch {
        return `无法计算: ${expression}`;
      }
    },
    {
      name: "calculator",
      description: "执行数学计算。支持加减乘除、括号等基本运算。",
      schema: z.object({
        expression: z.string().describe("数学表达式,例如:3 + 5 * 2"),
      }),
    }
  );

  console.log("🔧 已创建 2 个工具:get_weather, calculator\n");

  const model = new ChatDeepSeek({
    model: config.model,
    apiKey: config.apiKey,
    temperature: 0,
  });

  // ─── 2. 绑定工具 ─────────────────
  // bindTools() 告诉模型:"你可以使用这些工具"
  // 返回一个新的 Runnable,调用时会自动带上工具定义
  const modelWithTools = model.bindTools([getWeather, calculator]);

  // ─── 3. 演示:模型自动决定是否调用工具 ─────────────────

  // 场景 A:需要工具的问题 —— 模型会返回 tool_calls
  console.log("📌 场景 A:模型识别需要调用工具\n");
  console.log('💬 用户: 北京今天天气怎么样?');

  const responseA = await modelWithTools.invoke([
    new HumanMessage("北京今天天气怎么样?"),
  ]);

  const toolCallsA = responseA.tool_calls ?? [];
  console.log(`🤖 模型决定: ${toolCallsA.length > 0 ? "🔧 调用工具" : "💬 直接回答"}`);
  if (toolCallsA.length > 0) {
    for (const tc of toolCallsA) {
      console.log(`   工具名: ${tc.name}`);
      console.log(`   参数:   ${JSON.stringify(tc.args)}`);
    }
  }

  // 场景 B:不需要工具的问题 —— 模型会直接回答
  console.log('\n📌 场景 B:模型识别不需要工具\n');
  console.log('💬 用户: 什么是 TypeScript?');

  const responseB = await modelWithTools.invoke([
    new HumanMessage("什么是 TypeScript?"),
  ]);

  const toolCallsB = responseB.tool_calls ?? [];
  console.log(`🤖 模型决定: ${toolCallsB.length > 0 ? "🔧 调用工具" : "💬 直接回答"}`
  );
  console.log(`   回复: ${typeof responseB.content === 'string' ? responseB.content.substring(0, 100) + '...' : '[复杂内容]'}`);

  // ─── 4. 手动执行工具调用 ─────────────────
  console.log("\n📌 演示:手动执行工具调用\n");

  console.log('💬 用户: 北京天气怎么样?上海呢?(还需要算 123 * 456)');

  const responseC = await modelWithTools.invoke([
    new HumanMessage("北京天气怎么样?上海呢?(另外帮我算一下 123 * 456)"),
  ]);

  const toolCallsC = responseC.tool_calls ?? [];
  console.log(`🔧 模型想要调用 ${toolCallsC.length} 个工具:`);
  for (const tc of toolCallsC) {
    console.log(`   - ${tc.name}(${JSON.stringify(tc.args)})`);
  }

  // 执行工具并创建 ToolMessage
  const toolByName = new Map<string, StructuredToolInterface>([
    [getWeather.name, getWeather],
    [calculator.name, calculator],
  ]);

  for (const tc of toolCallsC) {
    const tool = toolByName.get(tc.name);
    if (!tool) {
      console.log(`   ⚠️ 未找到工具: ${tc.name}`);
      continue;
    }

    // invoke(toolCall) 会自动解析参数、调用函数、返回 ToolMessage
    const toolMessage = await tool.invoke(tc);
    console.log(`   ✅ ${tc.name} 结果: ${toolMessage.content}`);
  }

使用model.bindTools可以让模型使用绑定的工具。大模型在接受到信息之后,可以自己判断是否要是用工具,在定义工具的时候定义了description,大模型可以根据description判断是否要使用工具。

基础Agent

一个基础的LLM,现在有了记忆,有了工具,接下来只需要一个思考的循环,就可以成为一个agent,把以上的内容串起来,并给他一个循环,就是基础的Agent了。

const getWeather = tool(
  async ({ location }) => {
    const weatherData: Record<string, string> = {
      "北京": "晴天,25°C,湿度 40%,北风 2 级",
      "上海": "多云,28°C,湿度 65%,东南风 3 级",
      "深圳": "阵雨,30°C,湿度 80%,西南风 2 级",
      "杭州": "晴转多云,26°C,湿度 55%,东风 2 级",
    };
    return weatherData[location] ?? `未找到 ${location} 的天气数据(已知城市:北京、上海、深圳、杭州)`;
  },
  {
    name: "get_weather",
    description: "获取指定中国城市的当前天气信息",
    schema: z.object({
      location: z.string().describe("城市名称,例如:北京、上海、深圳"),
    }),
  }
);

const calculator = tool(
  async ({ expression }) => {
    try {
      const result = Function(`"use strict"; return (${expression})`)();
      return `${expression} = ${result}`;
    } catch {
      return `无法计算表达式: ${expression}`;
    }
  },
  {
    name: "calculator",
    description: "执行数学计算,支持加减乘除、括号、幂运算等",
    schema: z.object({
      expression: z.string().describe("数学表达式,例如:(3 + 5) * 2"),
    }),
  }
);

// ─── Agent 循环 ───────────────────────────────

/**
 * Agent 循环的核心实现
 *
 * ReAct 模式:Reasoning(推理)+ Acting(行动)
 *
 * 循环流程:
 * 1. LLM 接收消息 → 思考(Reasoning)
 * 2. 如果决定调用工具 → 行动(Acting)
 * 3. 工具返回结果 → 观察(Observation)
 * 4. 重复 1-3,直到 LLM 给出最终答案
 */
async function runAgent(
  model: ChatDeepSeek,
  tools: StructuredToolInterface[],
  userInput: string,
  options: {
    maxIterations?: number;
    verbose?: boolean;
  } = {}
): Promise<{ content: string; messages: BaseMessage[]; iterations: number }> {
  const { maxIterations = 10, verbose = true } = options;

  // 步骤 1:构建工具注册表(按名称索引)
  const toolByName = new Map(tools.map((t) => [t.name, t]));
  const toolNames = tools.map((t) => t.name).join(", ");

  // 步骤 2:绑定工具到模型
  const modelWithTools = model.bindTools(tools);

  // 步骤 3:初始化消息列表
  const systemPrompt = `你是一个智能助手,可以使用以下工具来帮助用户:
- ${tools.map((t) => `${t.name}: ${t.description}`).join("\n- ")}

使用工具时:
1. 如果需要信息,主动调用合适的工具
2. 基于工具返回的结果给出准确的回答
3. 如果工具返回错误,尝试其他方法或如实告知用户`;

  const messages: BaseMessage[] = [
    new SystemMessage(systemPrompt),
    new HumanMessage(userInput),
  ];

  if (verbose) {
    console.log("🤖 Agent 启动!");
    console.log(`🔧 可用工具: ${toolNames}`);
    console.log(`📝 用户输入: ${userInput}\n`);
  }

  // 步骤 4:Agent 主循环
  let iterations = 0;

  while (iterations < maxIterations) {
    iterations++;

    if (verbose) {
      console.log(`─── 第 ${iterations} 轮迭代 ───`);
    }

    // 4a. 调用 LLM
    const response: AIMessage = await modelWithTools.invoke(messages);
    messages.push(response);

    // 4b. 检查是否有工具调用
    if (!response.tool_calls || response.tool_calls.length === 0) {
      // ✅ 没有工具调用 → LLM 给出了最终答案
      if (verbose) {
        console.log(`✅ Agent 完成(共 ${iterations} 轮迭代)\n`);
      }
      return {
        content: typeof response.content === "string" ? response.content : "[复杂内容]",
        messages,
        iterations,
      };
    }

    // 4c. 有工具调用 → 执行工具
    if (verbose) {
      console.log(`🔧 LLM 想要调用 ${response.tool_calls.length} 个工具:`);
    }

    for (const tc of response.tool_calls) {
      const tool = toolByName.get(tc.name);

      if (verbose) {
        console.log(`   📞 调用: ${tc.name}(${JSON.stringify(tc.args)})`);
      }

      if (!tool) {
        // 工具未找到
        const errorMsg = `错误:未找到工具 "${tc.name}"。可用工具:${toolNames}`;
        if (verbose) console.log(`   ❌ ${errorMsg}`);
        messages.push(
          new ToolMessage({
            content: errorMsg,
            tool_call_id: tc.id!,
            name: tc.name,
          })
        );
        continue;
      }

      try {
        // invoke(toolCall) → 自动解析参数、调用函数、返回 ToolMessage
        const toolMessage = await tool.invoke(tc);
        if (verbose) {
          const preview =
            typeof toolMessage.content === "string"
              ? toolMessage.content.substring(0, 80)
              : "[复杂内容]";
          console.log(`   ✅ 结果: ${preview}`);
        }
        messages.push(toolMessage);
      } catch (err) {
        const errorMsg = `工具执行出错: ${err instanceof Error ? err.message : String(err)}`;
        if (verbose) console.log(`   ❌ ${errorMsg}`);
        messages.push(
          new ToolMessage({
            content: errorMsg,
            tool_call_id: tc.id!,
            name: tc.name,
          })
        );
      }
    }

    if (verbose) console.log(); // 空行
  }

  // 达到最大迭代次数 → 强制要求 LLM 总结
  if (verbose) {
    console.log(`⚠️ 达到最大迭代次数 (${maxIterations}),强制要求总结`);
  }

  messages.push(
    new HumanMessage("请基于已有的工具调用结果,给出最终答案。不要继续调用工具。")
  );
  const finalResponse = await model.invoke(messages);

  return {
    content: typeof finalResponse.content === "string" ? finalResponse.content : "[复杂内容]",
    messages,
    iterations,
  };
}

// ─── 主函数 ───────────────────────────────────

async function main() {
  console.log("═══════════════════════════════════════");
  console.log("  步骤 5:手写 Agent 循环 ⭐");
  console.log("═══════════════════════════════════════\n");

  validateConfig();

  const model = new ChatDeepSeek({
    model: config.model,
    apiKey: config.apiKey,
    temperature: 0, // 温度设为 0 让工具调用更可预测
  });

  const tools = [getWeather, calculator];

  // ─── 测试场景 ─────────────────

  const testCases = [
    "北京今天天气怎么样?",
    "北京的温度加上上海温度(假设上海28°C)是多少?",
  ];

  for (const input of testCases) {
    console.log("━".repeat(50));
    const result = await runAgent(model, tools, input, {
      maxIterations: 10,
      verbose: true,
    });

    console.log(`💬 用户: ${input}`);
    console.log(`🤖 Agent: ${result.content}`);
    console.log(`📊 统计: ${result.iterations} 轮迭代, ${result.messages.length} 条消息\n`);
  }
}

main().catch((err) => {
  console.error("❌ 出错:", err.message);
  process.exit(1);
});

以上runAgent中,有一个while循环,每次循环调用一次invoke,根据invoke中是否使用工具的结果,判断是否进入下一次循环或者退出。

总结

以上就是基本的Agent知识学习。

  • 如何调用大模型
  • 使用消息和历史使得大模型具有记忆
  • 让大模型使用工具
  • 基本的循环