Web Search Agent
这个示例构建一个可以搜索网页的 agent:
- 用户提出问题。
- LLM 判断是否需要调用
web_search。 - Tool 调用 Exa hosted MCP server。
- 搜索结果返回给 LLM。
- LLM 基于搜索结果生成最终回答。
示例使用 Exa hosted MCP endpoint:https://mcp.exa.ai/mcp,因此不需要 Exa API key。
ts
import {
anthropic,
createAgent,
memoryStorage,
tool,
} from "duclaw-cli/sdk";
type ExaSearchResult = {
title?: string;
url?: string;
text?: string;
publishedDate?: string;
};
type JsonRpcResponse<T> = {
result?: T;
error?: {
message?: string;
};
};
const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
// 辅助代码:调用 Exa hosted MCP server。这是 adapter 代码,不是 SDK 的特殊逻辑。
async function callExaTool<T>(name: string, args: Record<string, unknown>): Promise<T> {
const response = await fetch(EXA_MCP_URL, {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json, text/event-stream",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: crypto.randomUUID(),
method: "tools/call",
params: {
name,
arguments: args,
},
}),
});
if (!response.ok) {
throw new Error(`Exa MCP request failed: ${response.status}`);
}
const body = await response.text();
const payload = parseMcpResponse<JsonRpcResponse<T>>(body);
if (payload.error) {
throw new Error(payload.error.message ?? "Exa MCP tool call failed");
}
if (!payload.result) {
throw new Error("Exa MCP returned an empty result");
}
return payload.result;
}
// 辅助代码:Exa MCP 可能返回 JSON,也可能返回 text/event-stream message。
function parseMcpResponse<T>(body: string): T {
const eventData = body
.split("\n")
.find((line) => line.startsWith("data: "))
?.slice("data: ".length);
return JSON.parse(eventData ?? body) as T;
}
// SDK 关键点:tool 是 LLM 可以在 agent loop 中选择调用的类型化能力。
const webSearch = tool({
name: "web_search",
description: "Search the web for current information and return concise results.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "The web search query.",
},
numResults: {
type: "number",
description: "Number of search results to return.",
},
},
required: ["query"],
additionalProperties: false,
},
async run({ query, numResults = 5 }) {
// Adapter 边界:tool 可以调用任意外部服务,并把结构化结果返回给 LLM。
const result = await callExaTool<{ content?: Array<{ text?: string }> }>(
"web_search_exa",
{
query,
numResults,
type: "fast",
},
);
const text = result.content?.map((item) => item.text).filter(Boolean).join("\n\n") ?? "";
return normalizeSearchResults(text);
},
});
// 辅助代码:把 provider-specific output 规范化,再交给 LLM。
function normalizeSearchResults(raw: string): ExaSearchResult[] {
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed.results) ? parsed.results : [];
} catch {
return [{ title: "Search results", text: raw }];
}
}
// SDK 关键点:宿主应用显式注入 model、storage、tools 和 system prompt。
const agent = createAgent({
model: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }),
storage: memoryStorage(),
tools: [webSearch],
system: [
"You answer developer questions with current web context when needed.",
"Use web_search for recent facts, package versions, docs, or news.",
"Cite source URLs from the search results in the final answer.",
].join("\n"),
});
// 应用代码:把用户输入交给 agent loop,并输出最终 LLM 回答。
async function main() {
const result = await agent.run({
input: "What changed in the latest VitePress release?",
context: {
userId: "developer",
requestId: crypto.randomUUID(),
},
});
console.log(result.output);
}
void main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Tool Loop
SDK 负责 agent loop。应用负责提供 model、storage 和 tools:
txt
用户输入
-> createAgent(...).run()
-> LLM 选择 web_search
-> Exa MCP 返回搜索结果
-> LLM 接收 tool result
-> 最终回答关键边界是:web_search 只是一个普通 SDK tool。你可以把 Exa 替换成其它搜索服务,而不需要改变 agent API。