「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 との使い分けや、よくあるアンチパターンにも踏み込みます。
- LangChain とは何か / なぜ2026年でも選ばれているのか
- 環境構築〜インストールと最初のチャット
- ChatPromptTemplate と Output Parser
- LCEL(LangChain Expression Language)を深く知る
- Tool 使用と Structured Outputs
- Memory(会話履歴の保持)
- Agent 実装〜createToolCallingAgent と AgentExecutor
- LangGraph で状態機械として組む
- RAG(Retrieval-Augmented Generation)を組む
- 実用 Agent パターン
- LangSmith でトレース・評価する
- テスト戦略とアンチパターン
- LangChain.js vs Vercel AI SDK / 生 OpenAI SDK
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 の真価は「ChatOpenAI を ChatAnthropic に置き換えるだけで全部動く」点にあります。これは 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 の無限ループ:AgentExecutor は maxIterations: 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 のような最新領域は独学だと情報の鮮度判断が難しいため、メンターに最新リファレンスの読み方ごと教わるのが結果的に最短です。

コメント