LangChain完全実践ガイド〜LCEL・Agent・LangGraph・LangSmith・実装パターン〜

「OpenAI SDK でひととおり ChatGPT を呼べるようになった。でも Agent や RAG、ツール連携、ワークフロー分岐をプロダクションで書くなら何を使えばいいのか?」——その答えのひとつが LangChain です。本記事は LangChain.js(JavaScript / TypeScript 版)に完全特化し、LCEL(LangChain Expression Language)、Agent、LangGraph、LangSmith、RAG、Vector Store、Streaming、Structured Output、Memory、Human-in-the-loop までを、コピペで動く TypeScript コードを 40本以上 並べて解説します。「AIツールを比較する」記事ではなく、「LangChain.js でプロダクションレベルの LLM アプリを組み上げる」ための完全実践ガイドです。Vercel AI SDK との使い分けや、よくあるアンチパターンにも踏み込みます。

  1. LangChain とは何か / なぜ2026年でも選ばれているのか
    1. LangChain.js が解決する3つの痛み
    2. 本記事で構築するもの
  2. 環境構築〜インストールと最初のチャット
    1. プロジェクト初期化
    2. LangChain.js のパッケージ群をインストール
    3. .env を用意する
    4. 最小のチャット呼び出し
    5. Anthropic / Google モデルを差し替える
  3. ChatPromptTemplate と Output Parser
    1. プロンプトをテンプレ化する
    2. StringOutputParser で .content を剥がす
    3. JSON Output Parser
    4. Zod による型付き構造化出力(withStructuredOutput)
  4. LCEL(LangChain Expression Language)を深く知る
    1. Runnable インターフェースの統一性
    2. RunnableSequence で複数 Runnable を直列に
    3. RunnableParallel で複数生成を一気に
    4. RunnablePassthrough で入力を保持しつつ追加情報を注入
    5. RunnableLambda で任意関数を差し込む
    6. Streaming(.stream)で逐次表示
    7. .batch() で複数入力を並列実行
    8. .withRetry() / .withFallbacks() でリカバリを宣言的に
  5. Tool 使用と Structured Outputs
    1. Tool を Zod で定義する
    2. bindTools でモデルにツールを認識させる
    3. 手動で Tool を実行して結果を返すループ
    4. 複数ツールを束ねる
  6. Memory(会話履歴の保持)
    1. MessagesPlaceholder で履歴を差し込む
    2. RunnableWithMessageHistory で履歴を自動管理
    3. BufferMemory / SummaryMemory(legacy)
  7. Agent 実装〜createToolCallingAgent と AgentExecutor
    1. Tool Calling Agent の最短コード
    2. createReactAgent(LangGraph 版)を使う
  8. LangGraph で状態機械として組む
    1. StateGraph の最小例(分類 → 分岐)
    2. Conditional Edges で動的ルーティング
    3. Checkpointing(中断と再開)
    4. Human-in-the-loop(承認待ちで一時停止)
    5. マルチエージェント(Supervisor パターン)
  9. RAG(Retrieval-Augmented Generation)を組む
    1. Document Loader と Text Splitter
    2. PDF Loader
    3. MemoryVectorStore に登録
    4. Pinecone Vector Store(本番想定)
    5. Retriever を Runnable として LCEL に組み込む
    6. Conversational Retrieval(会話を踏まえた質問書き換え)
  10. 実用 Agent パターン
    1. SQL Agent(SQLite を自然言語で問い合わせ)
    2. CSV Agent
    3. Web 検索 Agent(Tavily 連携)
    4. ファイル書き込みツール
  11. LangSmith でトレース・評価する
    1. 環境変数だけで自動トレース
    2. traceable() で任意関数も計測対象に
    3. 評価データセットを実行する
  12. テスト戦略とアンチパターン
    1. FakeChatModel でユニットテスト
    2. Vitest + 録画再生で API 呼び出しを抑える
    3. 避けたいアンチパターン
  13. LangChain.js vs Vercel AI SDK / 生 OpenAI SDK
    1. 本記事のまとめ
    2. 学習を加速するなら

LangChain とは何か / なぜ2026年でも選ばれているのか

LangChain は LLM アプリのための「組み立てキット」です。プロンプト・モデル・パーサ・ツール・メモリ・検索器(Retriever)・状態機械(Graph)といった部品をそれぞれ独立したRunnableとして用意し、それらを「| 演算子」のように連結して 1本のパイプラインに組み上げます。2026年は @langchain/core + @langchain/openai など細分パッケージ + LangGraph + LangSmith という構成が標準です。生の OpenAI SDK だけで Agent を書くと数百行になりますが、LangChain.js なら 30 行で再利用可能な Agent が組めます。

LangChain.js が解決する3つの痛み

1) プロバイダ依存:OpenAI / Anthropic / Google / Mistral の差を吸収。2) 制御フローの煩雑さ:if-else・ループ・並列・retry を Runnable で宣言的に書ける。3) 観測の難しさ:LangSmith に自動でトレースが流れ、各ステップの入出力・トークン・レイテンシが可視化される。Vercel AI SDK が「UI から LLM を呼ぶ」用途に強い一方、LangChain.js は「LLM 同士・ツール・DB を絡めた多段ワークフロー」に強みがあります。

本記事で構築するもの

シンプルな1問1答チェーンから始め、LCEL での合成、Tool 連携 Agent、LangGraph でのマルチエージェント、PDF を読む RAG、LangSmith でのトレース可視化まで、段階的に積み上げます。途中の各コードは独立して動くようにしてあります。

環境構築〜インストールと最初のチャット

プロジェクト初期化

mkdir my-langchain-app && cd my-langchain-app
pnpm init
pnpm add typescript tsx @types/node -D
npx tsc --init --target es2022 --module nodenext --moduleResolution nodenext
echo 'node_modulesn.envn.langgraph_api' > .gitignore

LangChain.js のパッケージ群をインストール

# コア(必須)
pnpm add @langchain/core langchain
# モデルプロバイダ(必要なものだけ)
pnpm add @langchain/openai @langchain/anthropic @langchain/google-genai
# コミュニティ(検索/DB/ツール)
pnpm add @langchain/community
# LangGraph(状態機械)
pnpm add @langchain/langgraph
# LangSmith(観測)
pnpm add langsmith
# 補助
pnpm add zod dotenv

.env を用意する

# .env
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_API_KEY=...
# LangSmith(あとで使う)
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=ls-...
LANGCHAIN_PROJECT=my-langchain-app

最小のチャット呼び出し

// src/01_hello.ts
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0.2,
});

const res = await model.invoke("TypeScriptの良さを3行で説明して");
console.log(res.content);
npx tsx src/01_hello.ts

Anthropic / Google モデルを差し替える

// src/02_providers.ts
import "dotenv/config";
import { ChatAnthropic } from "@langchain/anthropic";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";

const claude = new ChatAnthropic({
  model: "claude-sonnet-4-5",
  temperature: 0,
});
const gemini = new ChatGoogleGenerativeAI({
  model: "gemini-2.0-flash",
  temperature: 0,
});

console.log((await claude.invoke("こんにちは")).content);
console.log((await gemini.invoke("こんにちは")).content);

LangChain.js の真価は「ChatOpenAIChatAnthropic に置き換えるだけで全部動く」点にあります。これは BaseChatModel という抽象クラスが共通インターフェースを提供しているからです。

ChatPromptTemplate と Output Parser

プロンプトをテンプレ化する

// src/03_prompt.ts
import { ChatPromptTemplate } from "@langchain/core/prompts";

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "あなたは{language}の専門家です。簡潔に答えてください。"],
  ["human", "{question}"],
]);

const messages = await prompt.formatMessages({
  language: "TypeScript",
  question: "satisfies 演算子とは?",
});
console.log(messages);

StringOutputParser で .content を剥がす

// src/04_parser.ts
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

const prompt = ChatPromptTemplate.fromTemplate("{topic}を100字で説明して");
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const parser = new StringOutputParser();

// LCEL: pipe で連結
const chain = prompt.pipe(model).pipe(parser);
const text: string = await chain.invoke({ topic: "Reactのhydration" });
console.log(text);

JSON Output Parser

// src/05_json_parser.ts
import { JsonOutputParser } from "@langchain/core/output_parsers";
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";

type Recipe = { name: string; steps: string[]; minutes: number };

const prompt = ChatPromptTemplate.fromTemplate(
  "料理「{dish}」のレシピをJSONで返して。フィールド: name, steps[], minutes"
);
const model = new ChatOpenAI({ model: "gpt-4o-mini" })
  .bind({ response_format: { type: "json_object" } });

const chain = prompt.pipe(model).pipe(new JsonOutputParser<Recipe>());
const recipe = await chain.invoke({ dish: "親子丼" });
console.log(recipe.name, recipe.minutes);

Zod による型付き構造化出力(withStructuredOutput)

// src/06_structured.ts
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";

const schema = z.object({
  title: z.string(),
  tags: z.array(z.string()).max(5),
  summary: z.string().describe("80字以内の要約"),
});

const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const structured = model.withStructuredOutput(schema, { name: "Article" });

const result = await structured.invoke(
  "TypeScript の satisfies について記事メタデータを生成して"
);
// result は z.infer<typeof schema> の型を持つ
console.log(result.tags);

withStructuredOutput は内部で function calling か JSON Schema 強制を使い、戻り値が Zod スキーマで型付けされる最重要 API です。Output Parser を手書きする時代は終わっています。

LCEL(LangChain Expression Language)を深く知る

LCEL は「Runnable を .pipe() で連結し、.invoke() / .stream() / .batch() で実行する DSL」です。全 Runnable は同じインターフェースを持ち、Streaming/並列/再試行が自動で配線されます。

Runnable インターフェースの統一性

// src/07_runnable.ts
import { RunnableLambda } from "@langchain/core/runnables";

const toUpper = RunnableLambda.from((s: string) => s.toUpperCase());
const exclaim = RunnableLambda.from((s: string) => s + "!!!");
const chain = toUpper.pipe(exclaim);

console.log(await chain.invoke("hello"));            // HELLO!!!
console.log(await chain.batch(["a", "b", "c"]));     // [ 'A!!!', 'B!!!', 'C!!!' ]
for await (const c of await chain.stream("stream")) console.log(c);

RunnableSequence で複数 Runnable を直列に

// src/08_sequence.ts
import { RunnableSequence } from "@langchain/core/runnables";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatOpenAI } from "@langchain/openai";

const seq = RunnableSequence.from([
  ChatPromptTemplate.fromTemplate("{topic} を関西弁で1行で"),
  new ChatOpenAI({ model: "gpt-4o-mini" }),
  new StringOutputParser(),
]);

console.log(await seq.invoke({ topic: "RustとGo" }));

RunnableParallel で複数生成を一気に

// src/09_parallel.ts
import { RunnableParallel } from "@langchain/core/runnables";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const parser = new StringOutputParser();

const joke = ChatPromptTemplate.fromTemplate("{topic}についてのジョーク")
  .pipe(model).pipe(parser);
const poem = ChatPromptTemplate.fromTemplate("{topic}についての俳句")
  .pipe(model).pipe(parser);

const parallel = RunnableParallel.from({ joke, poem });
const result = await parallel.invoke({ topic: "TypeScript" });
console.log(result.joke);
console.log(result.poem);

RunnablePassthrough で入力を保持しつつ追加情報を注入

// src/10_passthrough.ts
import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const chain = RunnableSequence.from([
  RunnablePassthrough.assign({
    upper: (input: { question: string }) => input.question.toUpperCase(),
  }),
  ChatPromptTemplate.fromTemplate(
    "原文: {question}n大文字化: {upper}n両方使って回答して"
  ),
  new ChatOpenAI({ model: "gpt-4o-mini" }),
  new StringOutputParser(),
]);

console.log(await chain.invoke({ question: "react hydration とは?" }));

RunnableLambda で任意関数を差し込む

// src/11_lambda.ts
import { RunnableLambda, RunnableSequence } from "@langchain/core/runnables";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const normalize = RunnableLambda.from((q: string) =>
  q.trim().replace(/s+/g, " ").slice(0, 200)
);

const chain = RunnableSequence.from([
  normalize,
  (q) => ({ question: q }),
  ChatPromptTemplate.fromTemplate("{question} に簡潔に答えて"),
  new ChatOpenAI({ model: "gpt-4o-mini" }),
  new StringOutputParser(),
]);

console.log(await chain.invoke("  TypeScript  と     JavaScriptの違い  "));

Streaming(.stream)で逐次表示

// src/12_stream.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

const chain = ChatPromptTemplate.fromTemplate("{q} を詳しく説明して")
  .pipe(new ChatOpenAI({ model: "gpt-4o-mini", streaming: true }))
  .pipe(new StringOutputParser());

for await (const chunk of await chain.stream({ q: "TCP/IP" })) {
  process.stdout.write(chunk);
}

.batch() で複数入力を並列実行

// src/13_batch.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

const chain = ChatPromptTemplate.fromTemplate("{w} を英訳して")
  .pipe(new ChatOpenAI({ model: "gpt-4o-mini" }))
  .pipe(new StringOutputParser());

const results = await chain.batch(
  [{ w: "ありがとう" }, { w: "さようなら" }, { w: "おはよう" }],
  { maxConcurrency: 3 }
);
console.log(results);

.withRetry() / .withFallbacks() でリカバリを宣言的に

// src/14_retry.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatAnthropic } from "@langchain/anthropic";

const primary = new ChatOpenAI({ model: "gpt-4o-mini" })
  .withRetry({ stopAfterAttempt: 3 });

const fallback = new ChatAnthropic({ model: "claude-3-5-haiku-latest" });

const robust = primary.withFallbacks([fallback]);
console.log((await robust.invoke("ping")).content);

Tool 使用と Structured Outputs

Tool を Zod で定義する

// src/15_tool.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";

export const getWeather = tool(
  async ({ city }) => {
    // 本来は外部API。ここでは固定値
    return JSON.stringify({ city, tempC: 22, condition: "sunny" });
  },
  {
    name: "get_weather",
    description: "指定都市の現在の天気を返す",
    schema: z.object({ city: z.string().describe("英語の都市名") }),
  }
);

bindTools でモデルにツールを認識させる

// src/16_bind_tools.ts
import { ChatOpenAI } from "@langchain/openai";
import { getWeather } from "./15_tool.js";

const model = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools([getWeather]);

const res = await model.invoke("Tokyo の天気を教えて");
console.log(res.tool_calls); // [{ name: 'get_weather', args: { city: 'Tokyo' }, id: '...' }]

手動で Tool を実行して結果を返すループ

// src/17_tool_loop.ts
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, ToolMessage } from "@langchain/core/messages";
import { getWeather } from "./15_tool.js";

const model = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools([getWeather]);
const messages = [new HumanMessage("Osakaの天気は?")];
const ai = await model.invoke(messages);
messages.push(ai);

for (const call of ai.tool_calls ?? []) {
  const result = await getWeather.invoke(call.args as any);
  messages.push(new ToolMessage({ tool_call_id: call.id!, content: result }));
}
const final = await model.invoke(messages);
console.log(final.content);

複数ツールを束ねる

// src/18_multi_tools.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";

export const addTool = tool(({ a, b }) => String(a + b), {
  name: "add", description: "2数を加算", schema: z.object({ a: z.number(), b: z.number() }),
});
export const mulTool = tool(({ a, b }) => String(a * b), {
  name: "mul", description: "2数を乗算", schema: z.object({ a: z.number(), b: z.number() }),
});
export const sqrtTool = tool(({ x }) => String(Math.sqrt(x)), {
  name: "sqrt", description: "平方根", schema: z.object({ x: z.number() }),
});

Memory(会話履歴の保持)

MessagesPlaceholder で履歴を差し込む

// src/19_history_prompt.ts
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";

export const chatPrompt = ChatPromptTemplate.fromMessages([
  ["system", "あなたは丁寧なアシスタント。"],
  new MessagesPlaceholder("history"),
  ["human", "{input}"],
]);

RunnableWithMessageHistory で履歴を自動管理

// src/20_history.ts
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { InMemoryChatMessageHistory } from "@langchain/core/chat_history";
import { ChatOpenAI } from "@langchain/openai";
import { chatPrompt } from "./19_history_prompt.js";

const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const chain = chatPrompt.pipe(model);

const sessions = new Map<string, InMemoryChatMessageHistory>();
const withHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: (id) => {
    if (!sessions.has(id)) sessions.set(id, new InMemoryChatMessageHistory());
    return sessions.get(id)!;
  },
  inputMessagesKey: "input",
  historyMessagesKey: "history",
});

const cfg = { configurable: { sessionId: "user-1" } };
console.log((await withHistory.invoke({ input: "私の名前は太郎" }, cfg)).content);
console.log((await withHistory.invoke({ input: "私の名前は?" }, cfg)).content);

BufferMemory / SummaryMemory(legacy)

// src/21_legacy_memory.ts
// langchain 旧API。新規は RunnableWithMessageHistory 推奨。
import { BufferMemory, ConversationSummaryMemory } from "langchain/memory";
import { ChatOpenAI } from "@langchain/openai";

const buffer = new BufferMemory({ memoryKey: "history", returnMessages: true });
const summary = new ConversationSummaryMemory({
  llm: new ChatOpenAI({ model: "gpt-4o-mini" }),
  memoryKey: "history",
});
await buffer.saveContext({ input: "好きな色は青" }, { output: "了解です" });
console.log((await buffer.loadMemoryVariables({})).history);

Agent 実装〜createToolCallingAgent と AgentExecutor

Tool Calling Agent の最短コード

// src/22_agent.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
import { addTool, mulTool, sqrtTool } from "./18_multi_tools.js";

const tools = [addTool, mulTool, sqrtTool];
const llm = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 });

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "あなたは計算アシスタント。必要なら tools を使え。"],
  new MessagesPlaceholder("chat_history"),
  ["human", "{input}"],
  new MessagesPlaceholder("agent_scratchpad"),
]);

const agent = await createToolCallingAgent({ llm, tools, prompt });
const executor = new AgentExecutor({ agent, tools, verbose: true });

const r = await executor.invoke({ input: "(3+4)*5 の平方根は?", chat_history: [] });
console.log(r.output);

createReactAgent(LangGraph 版)を使う

// src/23_react_agent.ts
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
import { addTool, mulTool, sqrtTool } from "./18_multi_tools.js";

const agent = createReactAgent({
  llm: new ChatOpenAI({ model: "gpt-4o-mini" }),
  tools: [addTool, mulTool, sqrtTool],
});

const res = await agent.invoke({
  messages: [{ role: "user", content: "100を3で割った余りに7を足して" }],
});
console.log(res.messages.at(-1)?.content);

新規の Agent 実装は createReactAgent(LangGraph 版)が推奨です。プレビルトでありながら内部実装は完全な StateGraph で、後述の Checkpointing や Human-in-the-loop もそのまま流用できます。

LangGraph で状態機械として組む

StateGraph の最小例(分類 → 分岐)

// src/24_graph_basic.ts
import { StateGraph, START, END, Annotation } from "@langchain/langgraph";

const State = Annotation.Root({
  input: Annotation<string>,
  category: Annotation<string>,
  answer: Annotation<string>,
});

const classify = async (s: typeof State.State) => {
  const c = /price|料金/.test(s.input) ? "billing" : "support";
  return { category: c };
};
const billing = async () => ({ answer: "料金は月額1980円です。" });
const support = async () => ({ answer: "サポート窓口にお繋ぎします。" });

const graph = new StateGraph(State)
  .addNode("classify", classify)
  .addNode("billing", billing)
  .addNode("support", support)
  .addEdge(START, "classify")
  .addConditionalEdges("classify", (s) => s.category, {
    billing: "billing",
    support: "support",
  })
  .addEdge("billing", END)
  .addEdge("support", END)
  .compile();

console.log(await graph.invoke({ input: "料金教えて" }));

Conditional Edges で動的ルーティング

// src/25_conditional.ts
import { StateGraph, START, END, Annotation } from "@langchain/langgraph";

const State = Annotation.Root({
  count: Annotation<number>({ reducer: (a, b) => a + b, default: () => 0 }),
});

const inc = async () => ({ count: 1 });
const route = (s: typeof State.State) => (s.count < 5 ? "inc" : END);

const g = new StateGraph(State)
  .addNode("inc", inc)
  .addEdge(START, "inc")
  .addConditionalEdges("inc", route)
  .compile();

console.log(await g.invoke({})); // { count: 5 }

Checkpointing(中断と再開)

// src/26_checkpoint.ts
import { StateGraph, START, END, Annotation } from "@langchain/langgraph";
import { MemorySaver } from "@langchain/langgraph";

const State = Annotation.Root({ messages: Annotation<string[]>({
  reducer: (a, b) => a.concat(b), default: () => [],
})});

const g = new StateGraph(State)
  .addNode("say", async (s) => ({ messages: ["hi " + s.messages.length] }))
  .addEdge(START, "say")
  .addEdge("say", END)
  .compile({ checkpointer: new MemorySaver() });

const cfg = { configurable: { thread_id: "t-1" } };
await g.invoke({ messages: [] }, cfg);
await g.invoke({ messages: [] }, cfg);
console.log((await g.getState(cfg)).values.messages); // ['hi 0', 'hi 1']

Human-in-the-loop(承認待ちで一時停止)

// src/27_hitl.ts
import { StateGraph, START, END, Annotation, MemorySaver, interrupt } from "@langchain/langgraph";

const State = Annotation.Root({
  draft: Annotation<string>,
  approved: Annotation<boolean>,
});

const writeDraft = async () => ({ draft: "送金しますがよろしいですか?" });
const askHuman = async (s: typeof State.State) => {
  const ok = interrupt({ question: s.draft });
  return { approved: ok as boolean };
};
const exec = async (s: typeof State.State) =>
  ({ draft: s.approved ? "送金しました" : "キャンセル" });

const g = new StateGraph(State)
  .addNode("writeDraft", writeDraft)
  .addNode("askHuman", askHuman)
  .addNode("exec", exec)
  .addEdge(START, "writeDraft")
  .addEdge("writeDraft", "askHuman")
  .addEdge("askHuman", "exec")
  .addEdge("exec", END)
  .compile({ checkpointer: new MemorySaver() });

const cfg = { configurable: { thread_id: "h-1" } };
console.log(await g.invoke({}, cfg));         // interrupt で一時停止
// 承認情報を渡して再開
import { Command } from "@langchain/langgraph";
console.log(await g.invoke(new Command({ resume: true }), cfg));

マルチエージェント(Supervisor パターン)

// src/28_supervisor.ts
import { StateGraph, START, END, Annotation, MessagesAnnotation } from "@langchain/langgraph";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
import { addTool, mulTool } from "./18_multi_tools.js";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
const mathAgent = createReactAgent({ llm, tools: [addTool, mulTool] });
const writeAgent = createReactAgent({ llm, tools: [] });

const supervise = async (s: typeof MessagesAnnotation.State) => {
  const last = String(s.messages.at(-1)?.content ?? "");
  return { next: /計算|足し|掛け/.test(last) ? "math" : "write" };
};

const State = Annotation.Root({
  ...MessagesAnnotation.spec,
  next: Annotation<string>,
});

const g = new StateGraph(State)
  .addNode("supervise", supervise)
  .addNode("math", mathAgent)
  .addNode("write", writeAgent)
  .addEdge(START, "supervise")
  .addConditionalEdges("supervise", (s) => s.next, { math: "math", write: "write" })
  .addEdge("math", END)
  .addEdge("write", END)
  .compile();

console.log(await g.invoke({ messages: [{ role: "user", content: "3+4を計算して" }] }));

RAG(Retrieval-Augmented Generation)を組む

Document Loader と Text Splitter

// src/29_loader_splitter.ts
import { TextLoader } from "langchain/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";

const docs = await new TextLoader("./data/manual.txt").load();
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 800,
  chunkOverlap: 100,
});
const chunks = await splitter.splitDocuments(docs);
console.log(chunks.length, chunks[0].pageContent.slice(0, 80));

PDF Loader

// src/30_pdf.ts
import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
const pages = await new PDFLoader("./data/spec.pdf").load();
console.log(`pages: ${pages.length}`);

MemoryVectorStore に登録

// src/31_memory_vector.ts
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { Document } from "@langchain/core/documents";

const docs = [
  new Document({ pageContent: "TypeScript は型を加えた JS 上位互換。" }),
  new Document({ pageContent: "React は UI 構築ライブラリ。" }),
  new Document({ pageContent: "LangChain.js は LLM 組み立てキット。" }),
];

const store = await MemoryVectorStore.fromDocuments(
  docs, new OpenAIEmbeddings({ model: "text-embedding-3-small" })
);

const hits = await store.similaritySearch("型がある言語は?", 2);
console.log(hits.map((d) => d.pageContent));

Pinecone Vector Store(本番想定)

// src/32_pinecone.ts
import { Pinecone } from "@pinecone-database/pinecone";
import { PineconeStore } from "@langchain/pinecone";
import { OpenAIEmbeddings } from "@langchain/openai";

const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
const index = pc.Index("my-rag");
const store = await PineconeStore.fromExistingIndex(
  new OpenAIEmbeddings({ model: "text-embedding-3-small" }),
  { pineconeIndex: index }
);
const hits = await store.similaritySearch("料金", 4);
console.log(hits.length);

Retriever を Runnable として LCEL に組み込む

// src/33_rag_chain.ts
import { RunnableSequence, RunnablePassthrough } from "@langchain/core/runnables";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { OpenAIEmbeddings } from "@langchain/openai";
import { Document } from "@langchain/core/documents";

const store = await MemoryVectorStore.fromDocuments(
  [
    new Document({ pageContent: "返品は購入から30日以内に限り受付。" }),
    new Document({ pageContent: "送料は3980円以上で無料。" }),
  ],
  new OpenAIEmbeddings({ model: "text-embedding-3-small" })
);
const retriever = store.asRetriever({ k: 3 });

const prompt = ChatPromptTemplate.fromTemplate(
  `次の文脈だけを参照して質問に答えて。n---n{context}n---n質問: {question}`
);

const formatDocs = (docs: Document[]) =>
  docs.map((d, i) => `[${i + 1}] ${d.pageContent}`).join("n");

const ragChain = RunnableSequence.from([
  {
    context: async (input: { question: string }) =>
      formatDocs(await retriever.invoke(input.question)),
    question: new RunnablePassthrough(),
  },
  prompt,
  new ChatOpenAI({ model: "gpt-4o-mini" }),
  new StringOutputParser(),
]);

console.log(await ragChain.invoke({ question: "送料無料の条件は?" }));

Conversational Retrieval(会話を踏まえた質問書き換え)

// src/34_conversational_rag.ts
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const rewritePrompt = ChatPromptTemplate.fromMessages([
  ["system", "会話履歴を踏まえ、検索に最適化したスタンドアロン質問に書き換えて。"],
  new MessagesPlaceholder("history"),
  ["human", "{question}"],
]);

const rewriter = rewritePrompt
  .pipe(new ChatOpenAI({ model: "gpt-4o-mini" }))
  .pipe(new StringOutputParser());

const rewritten = await rewriter.invoke({
  history: [{ role: "human", content: "送料は?" }, { role: "ai", content: "3980円以上で無料です。" }],
  question: "じゃあ返品は?",
});
console.log(rewritten); // "購入後の返品ポリシーは?" のように書き換わる

実用 Agent パターン

SQL Agent(SQLite を自然言語で問い合わせ)

// src/35_sql_agent.ts
import { SqlDatabase } from "langchain/sql_db";
import { DataSource } from "typeorm";
import { ChatOpenAI } from "@langchain/openai";
import { createSqlAgent, SqlToolkit } from "langchain/agents/toolkits/sql";

const ds = new DataSource({ type: "sqlite", database: "./data/shop.db" });
await ds.initialize();
const db = await SqlDatabase.fromDataSourceParams({ appDataSource: ds });

const llm = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 });
const toolkit = new SqlToolkit(db, llm);
const agent = createSqlAgent(llm, toolkit);

console.log(await agent.invoke({ input: "2025年の月別売上を出して" }));

CSV Agent

// src/36_csv_agent.ts
import { CSVLoader } from "@langchain/community/document_loaders/fs/csv";
import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const rows = (await new CSVLoader("./data/sales.csv").load())
  .map((d) => Object.fromEntries(d.pageContent.split("n").map((l) => l.split(": ") as [string, string])));

const queryCsv = tool(({ where }) => JSON.stringify(rows.slice(0, 5)), {
  name: "query_csv", description: "CSVの先頭数件を返す",
  schema: z.object({ where: z.string().optional() }),
});

const agent = createReactAgent({ llm: new ChatOpenAI({ model: "gpt-4o-mini" }), tools: [queryCsv] });
console.log(await agent.invoke({ messages: [{ role: "user", content: "CSVから上位5行を出して" }] }));

Web 検索 Agent(Tavily 連携)

// src/37_web_search.ts
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";

const search = new TavilySearchResults({ maxResults: 3 });
const agent = createReactAgent({
  llm: new ChatOpenAI({ model: "gpt-4o-mini" }),
  tools: [search],
});

const r = await agent.invoke({
  messages: [{ role: "user", content: "Bun の最新リリースを調べて要約して" }],
});
console.log(r.messages.at(-1)?.content);

ファイル書き込みツール

// src/38_file_tool.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { writeFile } from "node:fs/promises";

export const writeFileTool = tool(
  async ({ path, content }) => {
    await writeFile(path, content, "utf8");
    return `wrote ${path}`;
  },
  {
    name: "write_file",
    description: "ファイルを書き出す。pathは安全に。",
    schema: z.object({ path: z.string(), content: z.string() }),
  }
);

LangSmith でトレース・評価する

環境変数だけで自動トレース

# .env
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=ls-...
LANGCHAIN_PROJECT=my-langchain-app

この3行を入れるだけで、すべての .invoke() / .stream() が LangSmith に流れます。各 Runnable のノードごとに入出力・トークン数・所要時間が階層表示され、Agent が「どのツールをどの引数で呼んだか」が完全に追えるようになります。

traceable() で任意関数も計測対象に

// src/39_traceable.ts
import { traceable } from "langsmith/traceable";

const fetchUser = traceable(
  async (id: string) => ({ id, name: "taro" }),
  { name: "fetchUser" }
);

console.log(await fetchUser("u-1"));

評価データセットを実行する

// src/40_eval.ts
import { Client } from "langsmith";
import { evaluate } from "langsmith/evaluation";
import { ChatOpenAI } from "@langchain/openai";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
const target = async (input: { question: string }) =>
  ({ output: (await llm.invoke(input.question)).content });

await evaluate(target, {
  data: "qa-set-v1", // LangSmith 上のデータセット名
  evaluators: [
    async ({ run, example }) => ({
      key: "contains_keyword",
      score: String(run.outputs?.output ?? "").includes(example.outputs?.expected as string) ? 1 : 0,
    }),
  ],
});

テスト戦略とアンチパターン

FakeChatModel でユニットテスト

// src/41_fake.ts
import { FakeChatModel } from "@langchain/core/utils/testing";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

const fake = new FakeChatModel({});
const chain = ChatPromptTemplate.fromTemplate("{q}")
  .pipe(fake).pipe(new StringOutputParser());

// 決定的に動くのでスナップショット可
console.log(await chain.invoke({ q: "hello" }));

Vitest + 録画再生で API 呼び出しを抑える

// src/42_vitest.test.ts
import { describe, it, expect } from "vitest";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { FakeListChatModel } from "@langchain/core/utils/testing";

describe("greeting chain", () => {
  it("returns canned response", async () => {
    const llm = new FakeListChatModel({ responses: ["こんにちは!"] });
    const chain = ChatPromptTemplate.fromTemplate("{q}").pipe(llm);
    const r = await chain.invoke({ q: "hello" });
    expect(String(r.content)).toContain("こんにちは");
  });
});

避けたいアンチパターン

1) 巨大な単一 Runnable:すべてを 1 関数に詰めると LangSmith のトレースで何も読み取れません。意味のある粒度で RunnableSequence に分割しましょう。2) JSON.parse の手書き:LLM 出力の JSON 化は withStructuredOutput(z.object(...)) を使うべきで、try/catch で頑張らない。3) Agent の無限ループ:AgentExecutormaxIterations: 6 程度を必ず明示します。4) BufferMemory に全履歴を溜める:長期セッションでは trimMessages や Summary を併用。5) RAG で chunkSize を無計画に決める:800/100 を基準に、ドメインに合わせて A/B テスト。

LangChain.js vs Vercel AI SDK / 生 OpenAI SDK

Vercel AI SDK は「Next.js の UI から LLM を呼び、ストリーミングを useChat で受ける」用途に最適化されています。Tool 呼び出しや RAG も書けますが、複数エージェントの協調や分岐の多いワークフローでは記述量が増えます。LangChain.js + LangGraph は「サーバー側で複雑な意思決定パイプラインを長期運用する」のに強く、特に LangSmith の観測性は他で再現が難しい武器です。実プロダクトでは「Webアプリ層は Vercel AI SDK」「裏のオーケストレーションは LangChain.js + LangGraph」と分担する構成が増えています。

本記事のまとめ

LangChain.js を学ぶ近道は「Runnable と LCEL を体に馴染ませ、必要になったら Agent と LangGraph に拡張する」です。本記事の 40 本以上のコードを上から順に動かすだけで、Output Parser → LCEL → Tool → Memory → Agent → LangGraph → RAG → LangSmith の主要 API がひととおり手に入ります。LLM アプリは小さく動くものを早く作り、LangSmith のトレースを見ながら改善するループが何より重要です。コードを写経して、自分のドメインデータで RAG を組み、Agent に好きなツールを与えてみてください。

学習を加速するなら

LangChain.js を実務で書くには TypeScript / Node.js / 非同期処理 / ベクトル検索の基礎が前提になります。独学で詰まりやすい領域なので、体系的に学ぶならスクールの活用が近道です。AI / バックエンド領域に強いのは テックアカデミー(現役メンター・短期集中)、侍エンジニア(マンツーマンでカリキュラム自由設計)、DMM WEBCAMP(転職保証あり)、現役エンジニアの転職や副業案件獲得には レバテック(フリーランス・正社員双方の案件密度が高い)が定番です。LangChain.js のような最新領域は独学だと情報の鮮度判断が難しいため、メンターに最新リファレンスの読み方ごと教わるのが結果的に最短です。

コメント

タイトルとURLをコピーしました