本页内容
LangGraph 是 LangChain 用于构建有状态、基于图的 AI 智能体的框架。通过把 CrawlForge 工具作为图节点集成进来,你可以构建出能够智能决策的智能体:决定爬取什么、何时深入挖掘,以及如何在多个步骤中综合网页数据。
本指南将向你展示如何用 LangGraph 和 CrawlForge 在 TypeScript 中构建一个完整的爬取智能体。
目录
- 什么是 LangGraph?
- 前置条件
- 第 1 步:项目搭建
- 第 2 步:为 LangGraph 定义 CrawlForge 工具
- 第 3 步:设计智能体状态
- 第 4 步:构建图节点
- 第 5 步:连接整个图
- 第 6 步:运行智能体
- credits 成本参考
- LangGraph 与直接使用 LangChain 的对比
- 后续步骤
什么是 LangGraph?
LangGraph 是一个用于构建可靠 AI 智能体的低层编排框架。与简单的链式架构不同,LangGraph 将智能体逻辑建模为一个有向图,其中:
- 节点代表动作(工具调用、LLM 调用、数据处理)
- 边定义节点之间的转移,包括条件路由
- 状态在整个图的执行过程中持续存在
这种架构非常适合爬取智能体,因为网页爬取本质上涉及决策:我应该爬得更深吗?这个页面被拦截了吗?我需要切换到隐身模式吗?LangGraph 让你能够把这些决策建模为图中的条件边。
前置条件
- Node.js 18+ 和 TypeScript 5+
- 一个带有 API key 的 CrawlForge 账户(1,000 个免费 credits)
- 熟悉 LangChain 基础知识
第 1 步:项目搭建
mkdir langgraph-scraper && cd langgraph-scraper
npm init -y
npm install @langchain/langgraph @langchain/anthropic @langchain/core zod dotenv
npm install -D typescript @types/node tsx创建 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}将你的 API key 添加到 .env:
CRAWLFORGE_API_KEY=cf_live_your_key_here
ANTHROPIC_API_KEY=sk-ant-your_key_here第 2 步:为 LangGraph 定义 CrawlForge 工具
创建 LangGraph 可以调用的带类型的工具包装器:
// src/tools.ts
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
const CRAWLFORGE_API = 'https://crawlforge.dev/api/v1/tools';
async function callCrawlForge(
endpoint: string,
params: Record<string, unknown>
): Promise<string> {
const response = await fetch(`${CRAWLFORGE_API}/${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.CRAWLFORGE_API_KEY}`,
},
body: JSON.stringify(params),
});
if (!response.ok) {
return JSON.stringify({ error: `HTTP ${response.status}`, endpoint });
}
const data = await response.json();
return JSON.stringify(data);
}
export const searchWebTool = tool(
async ({ query, limit }) => {
return callCrawlForge('search_web', { query, limit });
},
{
name: 'search_web',
description: 'Search the web for information. Costs 5 credits. Use when you need to find URLs for a topic.',
schema: z.object({
query: z.string().describe('Search query'),
limit: z.number().default(5).describe('Max results'),
}),
}
);
export const extractContentTool = tool(
async ({ url }) => {
return callCrawlForge('extract_content', { url });
},
{
name: 'extract_content',
description: 'Extract clean readable content from a URL. Costs 2 credits.',
schema: z.object({
url: z.string().describe('URL to extract content from'),
}),
}
);
export const scrapeStructuredTool = tool(
async ({ url, selectors }) => {
return callCrawlForge('scrape_structured', { url, selectors });
},
{
name: 'scrape_structured',
description: 'Extract structured data using CSS selectors. Costs 2 credits.',
schema: z.object({
url: z.string().describe('URL to scrape'),
selectors: z.record(z.string()).describe('CSS selectors map'),
}),
}
);
export const fetchUrlTool = tool(
async ({ url }) => {
return callCrawlForge('fetch_url', { url });
},
{
name: 'fetch_url',
description: 'Fetch raw HTML from a URL. Cheapest option at 1 credit.',
schema: z.object({
url: z.string().describe('URL to fetch'),
}),
}
);
export const allTools = [
searchWebTool,
extractContentTool,
scrapeStructuredTool,
fetchUrlTool,
];第 3 步:设计智能体状态
LangGraph 智能体在图的执行过程中维护状态。定义一个用于跟踪爬取进度的状态结构:
// src/state.ts
import { BaseMessage } from '@langchain/core/messages';
import { Annotation } from '@langchain/langgraph';
// Define the graph state
export const AgentState = Annotation.Root({
// Conversation messages (LLM context)
messages: Annotation<BaseMessage[]>({
reducer: (prev, next) => [...prev, ...next],
default: () => [],
}),
// URLs discovered during research
discoveredUrls: Annotation<string[]>({
reducer: (prev, next) => [...new Set([...prev, ...next])],
default: () => [],
}),
// Content extracted from URLs
extractedContent: Annotation<Record<string, string>>({
reducer: (prev, next) => ({ ...prev, ...next }),
default: () => ({}),
}),
// Total credits consumed
creditsUsed: Annotation<number>({
reducer: (prev, next) => prev + next,
default: () => 0,
}),
// Current phase of the scraping pipeline
phase: Annotation<'search' | 'extract' | 'analyze' | 'complete'>({
reducer: (_prev, next) => next,
default: () => 'search' as const,
}),
});第 4 步:构建图节点
图中的每个节点执行一个特定的动作并更新状态:
// src/nodes.ts
import { ChatAnthropic } from '@langchain/anthropic';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { AgentState } from './state';
import { allTools } from './tools';
const model = new ChatAnthropic({
model: 'claude-sonnet-4-20250514',
temperature: 0,
}).bindTools(allTools);
// Node: LLM decides which tool to call next
export async function agentNode(
state: typeof AgentState.State
) {
const systemPrompt = new SystemMessage(
`You are a web research agent. Your goal is to find and extract information efficiently.
Always prefer cheaper tools: fetch_url (1cr) > extract_content (2cr) > search_web (5cr).
Track credits used. Stop when you have enough information or reach 20 credits.`
);
const response = await model.invoke([systemPrompt, ...state.messages]);
return { messages: [response] };
}
// Node: Execute tool calls
export const toolNode = new ToolNode(allTools);
// Node: Analyze extracted content and decide next step
export async function analyzeNode(
state: typeof AgentState.State
) {
const extractedCount = Object.keys(state.extractedContent).length;
if (extractedCount >= 3 || state.creditsUsed >= 20) {
return { phase: 'complete' as const };
}
return { phase: 'extract' as const };
}第 5 步:连接整个图
用边和条件路由把各个节点连接起来:
// src/graph.ts
import { StateGraph, END } from '@langchain/langgraph';
import { AgentState } from './state';
import { agentNode, toolNode, analyzeNode } from './nodes';
import { AIMessage } from '@langchain/core/messages';
// Determine if the agent wants to use a tool or is finished
function shouldContinue(state: typeof AgentState.State) {
const lastMessage = state.messages[state.messages.length - 1];
// If the LLM returned tool calls, route to tool execution
if (
lastMessage instanceof AIMessage &&
lastMessage.tool_calls &&
lastMessage.tool_calls.length > 0
) {
return 'tools';
}
// Otherwise, analyze what we have
return 'analyze';
}
// Determine if we should continue scraping or wrap up
function shouldFinish(state: typeof AgentState.State) {
if (state.phase === 'complete') {
return 'end';
}
return 'agent';
}
// Build the graph
const workflow = new StateGraph(AgentState)
// Add nodes
.addNode('agent', agentNode)
.addNode('tools', toolNode)
.addNode('analyze', analyzeNode)
// Set entry point
.addEdge('__start__', 'agent')
// Agent -> tools (if tool call) or analyze (if no tool call)
.addConditionalEdges('agent', shouldContinue, {
tools: 'tools',
analyze: 'analyze',
})
// Tools -> agent (return results to LLM)
.addEdge('tools', 'agent')
// Analyze -> agent (continue) or end (done)
.addConditionalEdges('analyze', shouldFinish, {
agent: 'agent',
end: END,
});
export const app = workflow.compile();第 6 步:运行智能体
// src/index.ts
import 'dotenv/config';
import { HumanMessage } from '@langchain/core/messages';
import { app } from './graph';
async function main() {
const result = await app.invoke({
messages: [
new HumanMessage(
'Research the top 3 MCP server implementations for web scraping. ' +
'Find their websites, extract their key features, and compare pricing.'
),
],
});
// Print final state
console.log('--- Research Complete ---');
console.log('Credits used:', result.creditsUsed);
console.log('URLs discovered:', result.discoveredUrls.length);
console.log('Pages extracted:', Object.keys(result.extractedContent).length);
console.log('\nFinal response:');
console.log(result.messages[result.messages.length - 1].content);
}
main().catch(console.error);运行它:
npx tsx src/index.ts该智能体会搜索网络、发现相关页面、从最有价值的结果中提取内容并综合出一份对比——而这一切都会在图状态中跟踪 credits 的用量。
credits 成本参考
| Credits | 工具 | LangGraph 中的节点角色 |
|---|---|---|
| 1 | fetch_url、extract_text、extract_links、extract_metadata | 轻量级的数据采集节点 |
| 2 | scrape_structured、extract_content、map_site、process_document、localization | 提取、发现和文档处理节点 |
| 3 | track_changes、analyze_content | 变更跟踪和分析节点 |
| 4 | summarize_content、crawl_deep | 摘要和多页爬取节点 |
| 5 | search_web、batch_scrape、scrape_with_actions、stealth_mode | 研究和批量操作节点 |
| 10 | deep_research | 全面分析(作为单节点子图使用) |
典型的 LangGraph 智能体运行:5(搜索)+ 6(3 次提取)+ 0(LLM 分析)= 11 credits。
LangGraph 与直接使用 LangChain 的对比
| 维度 | LangGraph | 直接使用 LangChain |
|---|---|---|
| 状态管理 | 内置、带类型、可持久化 | 手动实现,需要自定义代码 |
| 条件逻辑 | 一等的条件边 | 链函数中的 if/else |
| credits 跟踪 | 自动在图状态中跟踪 | 手动计数器 |
| 错误恢复 | 将错误路由到回退节点 | 链中的 try/catch |
| 复杂度 | 初始搭建成本较高 | 对线性工作流更简单 |
| 最适合 | 带分支逻辑的多步骤研究 | 简单的获取并处理流水线 |
当你的爬取智能体需要根据中间结果做决策时,使用 LangGraph。当工作流是线性的时候,使用直接的 LangChain(参见我们的 LangChain 集成指南)。
后续步骤
- LangGraph 文档 —— LangGraph 官方指南
- 用 CrawlForge 搭配 LangChain 的 5 种方式 —— 更简单的 LangChain 模式
- 构建研究助手 —— 相关的智能体架构
- CrawlForge API 参考 —— 完整的工具端点文档
今天就开始构建智能爬取智能体。 用 1,000 个免费 credits 注册 CrawlForge,把工具接入你的 LangGraph 图,让你的智能体决定接下来爬取什么。
亲自试一试——无需注册
在 Playground 中运行 CrawlForge 的 27 个抓取与提取工具中的任意一个,然后免费开始,获取 1,000 credits。
1,000 免费 credits • 每月补充 • 无需信用卡
标签
及时获取最新洞察
将教程、产品更新与 Web 抓取技巧直接发送到你的收件箱。
拒绝垃圾邮件,随时可取消订阅。