/
Update
20 min read
中文 如何使用 LangChain (TypeScript) 开发一个处理 PDF 文件的 Agent
如何使用 LangChain (TypeScript) 开发一个处理 PDF 文件的 Agent。这是一个完整的实战教程。
第一步:环境搭建#
1.1 初始化项目#
# 创建项目目录
mkdir pdf-agent-ts
cd pdf-agent-ts
# 初始化 npm 项目
npm init -y
# 安装 TypeScript 及相关工具
npm install -D typescript ts-node @types/node
# 初始化 TypeScript 配置
npx tsc --initbash1.2 生成的 tsconfig.json 需要修改以下配置#
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}json1.3 安装 LangChain 核心依赖#
# 核心框架
npm install langchain
# PDF 处理(PDFLoader 依赖)
npm install pdf-parse
# 向量数据库(本地开发用)
npm install @langchain/community
# OpenAI 相关(用于嵌入和对话模型)
npm install @langchain/openai
# 环境变量管理
npm install dotenvbash1.4 安装可选依赖(推荐)#
# 更好的文本分割(支持中文)
npm install @langchain/textsplitters
# 本地向量存储(开发测试用)
npm install faiss-node
# 或者使用内存向量存储(无需额外安装)bash1.5 项目目录结构#
pdf-agent-ts/
├── src/
│ ├── config/ # 配置文件
│ ├── loaders/ # 文档加载器
│ ├── splitters/ # 文本分割器
│ ├── vectorstores/ # 向量存储
│ ├── chains/ # 链定义
│ ├── agents/ # Agent 定义
│ ├── tools/ # 自定义工具
│ ├── utils/ # 工具函数
│ └── index.ts # 入口文件
├── data/ # 存放 PDF 文件
│ └── sample.pdf
├── .env # 环境变量
├── .env.example # 环境变量示例
├── tsconfig.json
└── package.jsonplaintext1.6 创建环境变量文件#
.env
# OpenAI API 密钥(必填)
OPENAI_API_KEY=sk-your-openai-api-key-here
# 可选:自定义 OpenAI 基础 URL(如果需要代理)
# OPENAI_BASE_URL=https://api.openai.com/v1
# 可选:LangSmith 追踪(用于调试)
# LANGCHAIN_TRACING_V2=true
# LANGCHAIN_API_KEY=ls-your-langsmith-key
# LANGCHAIN_PROJECT=pdf-agent-projectplaintext.env.example(用于团队协作模板)
OPENAI_API_KEY=
OPENAI_BASE_URL=
LANGCHAIN_TRACING_V2=
LANGCHAIN_API_KEY=
LANGCHAIN_PROJECT=plaintext第二步:配置模块#
2.1 创建配置文件 src/config/env.ts#
import { config } from "dotenv";
config();
/**
* 环境变量配置集中管理
* 所有配置项都在这里定义,避免散落在代码各处
*/
export const env = {
// OpenAI 配置
openai: {
apiKey: process.env.OPENAI_API_KEY!,
baseURL: process.env.OPENAI_BASE_URL || "https://api.openai.com/v1",
modelName: "gpt-4o-mini", // 性价比高的模型,可替换为 gpt-4o
embeddingModel: "text-embedding-3-small",
},
// LangSmith 追踪配置(可选,用于调试)
langsmith: {
tracing: process.env.LANGCHAIN_TRACING_V2 === "true",
apiKey: process.env.LANGCHAIN_API_KEY,
project: process.env.LANGCHAIN_PROJECT || "pdf-agent",
},
// 应用配置
app: {
chunkSize: 1000, // 文本分割块大小
chunkOverlap: 200, // 块之间重叠大小
topK: 4, // 检索返回的最相似文档数
temperature: 0.1, // 模型温度(越低越确定)
},
};
// 启动时验证必要环境变量
if (!env.openai.apiKey) {
throw new Error("❌ 错误:未设置 OPENAI_API_KEY 环境变量,请在 .env 文件中配置");
}
console.log("✅ 环境配置加载成功");typescript第三步:PDF 文档加载模块#
3.1 创建 PDF 加载器 src/loaders/pdfLoader.ts#
import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
import { Document } from "@langchain/core/documents";
import * as path from "path";
/**
* PDF 文档加载器
* 负责将 PDF 文件转换为 LangChain Document 对象
*/
export class PDFDocumentLoader {
private filePath: string;
constructor(filePath: string) {
this.filePath = path.resolve(filePath);
}
/**
* 加载 PDF 文件
* @param splitPages 是否按页分割(true=每页一个Document,false=整个PDF一个Document)
*/
async load(splitPages: boolean = true): Promise<Document[]> {
try {
console.log(`📄 正在加载 PDF: ${this.filePath}`);
const loader = new PDFLoader(this.filePath, {
splitPages, // 按页分割
parsedItemSeparator: "", // 解析项之间的分隔符
});
const documents = await loader.load();
console.log(`✅ 成功加载 ${documents.length} 个文档片段`);
// 打印前3个片段的信息用于调试
documents.slice(0, 3).forEach((doc, i) => {
console.log(`\n--- 片段 ${i + 1} ---`);
console.log(`页码: ${doc.metadata.loc?.pageNumber || "N/A"}`);
console.log(`内容长度: ${doc.pageContent.length} 字符`);
console.log(`内容预览: ${doc.pageContent.substring(0, 100)}...`);
});
return documents;
} catch (error) {
console.error(`❌ PDF 加载失败: ${error}`);
throw error;
}
}
/**
* 批量加载多个 PDF 文件
*/
static async loadMultiple(filePaths: string[]): Promise<Document[]> {
const allDocs: Document[] = [];
for (const filePath of filePaths) {
const loader = new PDFDocumentLoader(filePath);
const docs = await loader.load(true);
allDocs.push(...docs);
}
return allDocs;
}
}typescript第四步:文本分割模块#
4.1 创建文本分割器 src/splitters/textSplitter.ts#
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { Document } from "@langchain/core/documents";
import { env } from "../config/env";
/**
* 文本分割器
* 将长文档切分为适合向量化的 chunks
*/
export class DocumentSplitter {
private splitter: RecursiveCharacterTextSplitter;
constructor() {
// RecursiveCharacterTextSplitter 会按以下顺序尝试分割:
// 1. 段落(\n\n)
// 2. 换行(\n)
// 3. 句子(.!?)
// 4. 单词(空格)
// 5. 字符
// 这样可以尽量保持语义完整性
this.splitter = new RecursiveCharacterTextSplitter({
chunkSize: env.app.chunkSize, // 每个块的最大字符数
chunkOverlap: env.app.chunkOverlap, // 块之间的重叠字符数(保持上下文连贯)
separators: ["\n\n", "\n", ".", "!", "?", " ", ""], // 分割优先级
});
}
/**
* 分割文档
* @param documents 原始文档列表
*/
async split(documents: Document[]): Promise<Document[]> {
console.log(`✂️ 正在分割 ${documents.length} 个文档...`);
const splitDocs = await this.splitter.splitDocuments(documents);
console.log(`✅ 分割完成,共 ${splitDocs.length} 个片段`);
// 显示统计信息
const avgLength = splitDocs.reduce((sum, d) => sum + d.pageContent.length, 0) / splitDocs.length;
console.log(`📊 平均片段长度: ${Math.round(avgLength)} 字符`);
return splitDocs;
}
/**
* 自定义参数分割(用于不同场景)
*/
async splitWithOptions(
documents: Document[],
chunkSize: number,
chunkOverlap: number
): Promise<Document[]> {
const customSplitter = new RecursiveCharacterTextSplitter({
chunkSize,
chunkOverlap,
});
return await customSplitter.splitDocuments(documents);
}
}typescript第五步:向量存储模块#
5.1 创建向量存储 src/vectorstores/vectorStore.ts#
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { Document } from "@langchain/core/documents";
import { env } from "../config/env";
/**
* 向量存储管理器
* 负责文档的嵌入和向量检索
*/
export class VectorStoreManager {
private embeddings: OpenAIEmbeddings;
private vectorStore: MemoryVectorStore | null = null;
constructor() {
// 初始化 OpenAI 嵌入模型
this.embeddings = new OpenAIEmbeddings({
apiKey: env.openai.apiKey,
modelName: env.openai.embeddingModel,
// 可选:配置重试
maxRetries: 3,
});
}
/**
* 将文档添加到向量存储
* @param documents 要嵌入的文档片段
*/
async addDocuments(documents: Document[]): Promise<void> {
console.log(`🔢 正在为 ${documents.length} 个片段生成嵌入向量...`);
// MemoryVectorStore 适合开发和测试,数据存在内存中
this.vectorStore = await MemoryVectorStore.fromDocuments(
documents,
this.embeddings
);
console.log("✅ 向量存储构建完成");
}
/**
* 相似性搜索
* @param query 查询文本
* @param k 返回结果数量
*/
async similaritySearch(query: string, k: number = env.app.topK): Promise<Document[]> {
if (!this.vectorStore) {
throw new Error("❌ 向量存储未初始化,请先调用 addDocuments()");
}
console.log(`🔍 执行相似性搜索: "${query}"`);
const results = await this.vectorStore.similaritySearch(query, k);
console.log(`📋 找到 ${results.length} 个相关片段`);
results.forEach((doc, i) => {
console.log(`\n--- 结果 ${i + 1} ---`);
console.log(`来源: ${doc.metadata.source || "unknown"}`);
console.log(`页码: ${doc.metadata.loc?.pageNumber || "N/A"}`);
console.log(`内容: ${doc.pageContent.substring(0, 150)}...`);
});
return results;
}
/**
* 带分数的相似性搜索(返回相似度分数)
*/
async similaritySearchWithScore(query: string, k: number = env.app.topK) {
if (!this.vectorStore) {
throw new Error("❌ 向量存储未初始化");
}
return await this.vectorStore.similaritySearchWithScore(query, k);
}
/**
* 作为检索器使用(供 Chain/Agent 调用)
*/
asRetriever(k: number = env.app.topK) {
if (!this.vectorStore) {
throw new Error("❌ 向量存储未初始化");
}
return this.vectorStore.asRetriever({ k });
}
}typescript第六步:自定义工具模块#
6.1 创建 PDF 搜索工具 src/tools/pdfSearchTool.ts#
import { DynamicTool } from "@langchain/core/tools";
import { VectorStoreManager } from "../vectorstores/vectorStore";
/**
* 创建 PDF 内容搜索工具
* 供 Agent 调用,用于在 PDF 中查找相关信息
*/
export function createPDFSearchTool(vectorStoreManager: VectorStoreManager) {
return new DynamicTool({
name: "pdf_search",
description: `
在已上传的 PDF 文档中搜索相关信息。
输入应该是一个具体的搜索查询(问题或关键词)。
返回与查询最相关的文档片段。
当用户询问 PDF 内容时使用此工具。
`.trim(),
// 工具执行函数
func: async (input: string) => {
try {
const results = await vectorStoreManager.similaritySearch(input, 4);
// 将检索结果格式化为字符串返回给 Agent
const formattedResults = results
.map((doc, i) => {
const pageNum = doc.metadata.loc?.pageNumber || "未知";
return `[片段 ${i + 1}] (第 ${pageNum} 页)\n${doc.pageContent}`;
})
.join("\n\n---\n\n");
return formattedResults || "未找到相关信息";
} catch (error) {
return `搜索出错: ${error}`;
}
},
});
}
/**
* 创建文档摘要工具(简单版)
*/
export function createDocumentSummaryTool() {
return new DynamicTool({
name: "document_summary",
description: "提供当前已加载文档的基本信息和摘要。用于回答关于文档整体的问题。",
func: async () => {
return "当前已加载 PDF 文档,可以使用 pdf_search 工具查询具体内容。";
},
});
}typescript第七步:Agent 核心模块#
7.1 创建 PDF Agent src/agents/pdfAgent.ts#
import { ChatOpenAI } from "@langchain/openai";
import { createOpenAIFunctionsAgent, AgentExecutor } from "langchain/agents";
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
import { VectorStoreManager } from "../vectorstores/vectorStore";
import { createPDFSearchTool, createDocumentSummaryTool } from "../tools/pdfSearchTool";
import { env } from "../config/env";
/**
* PDF Agent 构建器
* 整合所有组件,构建可回答 PDF 相关问题的 Agent
*/
export class PDFAgentBuilder {
private vectorStoreManager: VectorStoreManager;
private model: ChatOpenAI;
constructor(vectorStoreManager: VectorStoreManager) {
this.vectorStoreManager = vectorStoreManager;
// 初始化对话模型
this.model = new ChatOpenAI({
apiKey: env.openai.apiKey,
modelName: env.openai.modelName,
temperature: env.app.temperature,
streaming: true, // 启用流式输出
});
}
/**
* 构建 Agent
*/
async build() {
// 1. 定义工具
const tools = [
createPDFSearchTool(this.vectorStoreManager),
createDocumentSummaryTool(),
];
// 2. 构建 Prompt 模板
// 使用 OpenAI Functions 格式的 Agent Prompt
const prompt = ChatPromptTemplate.fromMessages([
// 系统提示:定义 Agent 的角色和行为
[
"system",
`你是一个专业的 PDF 文档分析助手。你的任务是基于已上传的 PDF 文档内容回答用户问题。
规则:
1. 只使用提供给你的工具获取信息,不要编造答案
2. 如果文档中没有相关信息,请明确告知用户
3. 回答时引用具体的页码信息(如果有)
4. 保持回答简洁、准确、专业
当前可用工具:
- pdf_search: 在 PDF 中搜索具体内容
- document_summary: 获取文档基本信息
`,
],
// 历史消息占位符(支持多轮对话)
new MessagesPlaceholder("chat_history"),
// 用户输入
["human", "{input}"],
// Agent 思考过程占位符(OpenAI Functions Agent 需要)
new MessagesPlaceholder("agent_scratchpad"),
]);
// 3. 创建 Agent
const agent = await createOpenAIFunctionsAgent({
llm: this.model,
tools,
prompt,
});
// 4. 创建 Agent 执行器
const agentExecutor = new AgentExecutor({
agent,
tools,
verbose: true, // 打印详细执行日志(开发时开启)
maxIterations: 5, // 最大迭代次数,防止无限循环
returnIntermediateSteps: true, // 返回中间步骤(用于调试)
});
console.log("🤖 PDF Agent 构建完成");
return agentExecutor;
}
}typescript第八步:主流程编排#
8.1 创建主入口 src/index.ts#
import { PDFDocumentLoader } from "./loaders/pdfLoader";
import { DocumentSplitter } from "./splitters/textSplitter";
import { VectorStoreManager } from "./vectorstores/vectorStore";
import { PDFAgentBuilder } from "./agents/pdfAgent";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import * as readline from "readline";
/**
* PDF Agent 主程序
* 完整流程:加载 PDF → 分割 → 向量化 → 构建 Agent → 交互问答
*/
class PDFAgentApp {
private vectorStoreManager: VectorStoreManager;
private chatHistory: (HumanMessage | AIMessage)[] = [];
constructor() {
this.vectorStoreManager = new VectorStoreManager();
}
/**
* 初始化:加载并处理 PDF
*/
async initialize(pdfPath: string): Promise<void> {
console.log("🚀 开始初始化 PDF Agent...\n");
// 步骤 1: 加载 PDF
const loader = new PDFDocumentLoader(pdfPath);
const rawDocuments = await loader.load(true);
// 步骤 2: 分割文档
const splitter = new DocumentSplitter();
const splitDocuments = await splitter.split(rawDocuments);
// 步骤 3: 构建向量存储
await this.vectorStoreManager.addDocuments(splitDocuments);
console.log("\n✅ 初始化完成!可以开始提问了\n");
}
/**
* 单轮问答(非流式)
*/
async ask(question: string): Promise<string> {
// 构建 Agent
const builder = new PDFAgentBuilder(this.vectorStoreManager);
const agent = await builder.build();
// 执行
const result = await agent.invoke({
input: question,
chat_history: this.chatHistory,
});
// 保存对话历史
this.chatHistory.push(new HumanMessage(question));
this.chatHistory.push(new AIMessage(result.output));
// 限制历史长度(防止超出 Token 限制)
if (this.chatHistory.length > 10) {
this.chatHistory = this.chatHistory.slice(-10);
}
return result.output;
}
/**
* 流式问答(实时输出)
*/
async askStream(question: string): Promise<void> {
const builder = new PDFAgentBuilder(this.vectorStoreManager);
const agent = await builder.build();
console.log("🤖 Agent 思考中...\n");
const result = await agent.invoke({
input: question,
chat_history: this.chatHistory,
});
console.log("\n💡 回答:");
console.log(result.output);
// 保存历史
this.chatHistory.push(new HumanMessage(question));
this.chatHistory.push(new AIMessage(result.output));
}
/**
* 交互式命令行界面
*/
async startInteractive(): Promise<void> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log("💬 交互模式已启动");
console.log("输入问题直接提问,输入 'exit' 退出,输入 'clear' 清空历史\n");
const askQuestion = () => {
rl.question("你: ", async (input) => {
const trimmed = input.trim();
if (trimmed.toLowerCase() === "exit") {
console.log("👋 再见!");
rl.close();
return;
}
if (trimmed.toLowerCase() === "clear") {
this.chatHistory = [];
console.log("🧹 对话历史已清空\n");
askQuestion();
return;
}
if (!trimmed) {
askQuestion();
return;
}
try {
await this.askStream(trimmed);
} catch (error) {
console.error("❌ 出错了:", error);
}
console.log(); // 空行分隔
askQuestion();
});
};
askQuestion();
}
}
// ==================== 启动程序 ====================
async function main() {
// PDF 文件路径(可以是相对路径或绝对路径)
const pdfPath = process.argv[2] || "./data/sample.pdf";
const app = new PDFAgentApp();
try {
// 初始化
await app.initialize(pdfPath);
// 启动交互模式
await app.startInteractive();
} catch (error) {
console.error("❌ 程序启动失败:", error);
process.exit(1);
}
}
// 运行
main();typescript第九步:运行与测试#
9.1 准备测试 PDF#
将任意 PDF 文件放入 data/ 目录,命名为 sample.pdf,或者通过命令行参数指定路径。
9.2 修改 package.json 添加启动脚本#
{
"scripts": {
"start": "ts-node src/index.ts",
"start:file": "ts-node src/index.ts",
"build": "tsc",
"dev": "ts-node src/index.ts ./data/sample.pdf"
}
}json9.3 运行程序#
# 使用默认路径(./data/sample.pdf)
npm run dev
# 或指定 PDF 文件路径
npm run start -- ./data/another-document.pdfbash9.4 预期输出示例#
🚀 开始初始化 PDF Agent...
📄 正在加载 PDF: /path/to/pdf-agent-ts/data/sample.pdf
✅ 成功加载 5 个文档片段
--- 片段 1 ---
页码: 1
内容长度: 2450 字符
内容预览: 第一章 概述
本文档详细说明了...
✂️ 正在分割 5 个文档...
✅ 分割完成,共 12 个片段
📊 平均片段长度: 980 字符
🔢 正在为 12 个片段生成嵌入向量...
✅ 向量存储构建完成
✅ 初始化完成!可以开始提问了
💬 交互模式已启动
输入问题直接提问,输入 'exit' 退出,输入 'clear' 清空历史
你: 这份文档的主要内容是什么?
🤖 Agent 思考中...
[verbose 日志显示 Agent 调用 pdf_search 工具]
💡 回答:
根据文档内容,这是一份关于...
你: 第三章提到了哪些关键点?
...plaintext第十步:进阶优化(可选)#
10.1 添加更多工具 src/tools/advancedTools.ts#
import { DynamicTool } from "@langchain/core/tools";
/**
* 数学计算工具(处理 PDF 中的数据计算)
*/
export function createCalculatorTool() {
return new DynamicTool({
name: "calculator",
description: "进行数学计算。输入应该是一个数学表达式,如 '100 * 0.15' 或 '(500 + 300) / 2'",
func: async (input: string) => {
try {
// 安全计算:只允许基本运算符
const sanitized = input.replace(/[^0-9+\-*/().\s]/g, "");
// 使用 Function 构造器计算(生产环境建议使用更安全的计算库)
const result = new Function(`return (${sanitized})`)();
return `计算结果: ${result}`;
} catch {
return "计算失败,请检查表达式格式";
}
},
});
}
/**
* 当前日期工具
*/
export function createDateTool() {
return new DynamicTool({
name: "current_date",
description: "获取当前日期和时间,用于时间相关的回答",
func: async () => {
return new Date().toLocaleString("zh-CN");
},
});
}typescript10.2 使用 LangChain Expression Language (LCEL) 简化版本#
如果你想用更现代的 LCEL 方式(推荐用于生产环境),可以创建 src/chains/ragChain.ts:
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { createRetrievalChain } from "langchain/chains/retrieval";
import { Document } from "@langchain/core/documents";
import { env } from "../config/env";
/**
* 使用 LCEL 构建 RAG Chain(更简洁,性能更好)
*/
export async function buildRAGChain(documents: Document[]) {
// 1. 嵌入并存储
const embeddings = new OpenAIEmbeddings({
apiKey: env.openai.apiKey,
modelName: env.openai.embeddingModel,
});
const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings);
const retriever = vectorStore.asRetriever({ k: 4 });
// 2. 定义 Prompt
const prompt = ChatPromptTemplate.fromTemplate(`
基于以下上下文回答问题。如果上下文没有相关信息,请说"根据文档内容,我无法找到相关信息"。
上下文:
{context}
问题: {input}
请用中文回答,并标注信息来源页码。
`);
// 3. 创建文档组合 Chain
const combineDocsChain = await createStuffDocumentsChain({
llm: new ChatOpenAI({
apiKey: env.openai.apiKey,
modelName: env.openai.modelName,
temperature: 0.1,
}),
prompt,
});
// 4. 创建检索 Chain
const retrievalChain = await createRetrievalChain({
retriever,
combineDocsChain,
});
return retrievalChain;
}typescript10.3 持久化向量存储(生产环境)#
如果需要持久化数据,可以使用 Chroma 或 Pinecone:
# 安装 Chroma
npm install chromadb @langchain/community
# 或使用 Pinecone
npm install @pinecone-database/pineconebash// src/vectorstores/chromaStore.ts(示例)
import { Chroma } from "@langchain/community/vectorstores/chroma";
import { OpenAIEmbeddings } from "@langchain/openai";
export async function createChromaStore(documents: Document[]) {
return await Chroma.fromDocuments(documents, new OpenAIEmbeddings(), {
collectionName: "pdf-collection",
url: "http://localhost:8000", // Chroma 服务地址
});
}typescript完整文件清单#
| 文件路径 | 作用 |
|---|---|
src/config/env.ts | 环境变量与配置 |
src/loaders/pdfLoader.ts | PDF 文档加载 |
src/splitters/textSplitter.ts | 文本分割 |
src/vectorstores/vectorStore.ts | 向量存储管理 |
src/tools/pdfSearchTool.ts | 自定义搜索工具 |
src/agents/pdfAgent.ts | Agent 构建 |
src/index.ts | 主程序入口 |
关键概念总结#
| 组件 | 作用 | 类比 |
|---|---|---|
| Document Loader | 读取各种格式文件 | 图书管理员取书 |
| Text Splitter | 长文本切分 | 把书拆成章节 |
| Embeddings | 文本向量化 | 翻译成机器语言 |
| Vector Store | 存储和检索向量 | 智能图书馆索引 |
| Retriever | 相似性搜索 | 根据问题找相关章节 |
| Agent | 决策+工具调用 | 会查资料的研究员 |
| Tool | 具体功能模块 | 研究员的工具箱 |
如需进一步讲解某个模块(如流式输出、多 PDF 管理、部署到 Web 服务等),请告诉我!