發(fā) - 手寫(xiě)Manus之Tavily搜索工具:04 讓AI Agent接入互聯(lián)網(wǎng)(TaoToken 統(tǒng)一 Key 配置版))
1. 為什么你的 Java Agent 還差一雙“眼睛”大模型開(kāi)發(fā)走到手寫(xiě) Manus 這一步文件讀寫(xiě)和 Docker 沙箱代碼執(zhí)行都已經(jīng)跑通了但你會(huì)發(fā)現(xiàn) Agent 依然是個(gè)“閉卷考生”。你問(wèn)它“今天有什么值得關(guān)注的 AI 開(kāi)源項(xiàng)目”它只能從訓(xùn)練數(shù)據(jù)里翻舊賬給出的答案可能停留在幾個(gè)月前。這不是模型不夠聰明而是它缺少一個(gè)能實(shí)時(shí)訪問(wèn)互聯(lián)網(wǎng)的搜索工具。Tavily 就是為 AI Agent 量身定做的搜索 API。它不像傳統(tǒng)搜索引擎那樣返回一堆需要解析的 HTML而是直接給你結(jié)構(gòu)化的 JSON標(biāo)題、鏈接、摘要拿來(lái)就能塞進(jìn)大模型的上下文。對(duì)于 Java 手寫(xiě) Manus 的場(chǎng)景來(lái)說(shuō)這意味著你不需要寫(xiě)爬蟲(chóng)、不需要處理反爬、不需要清洗頁(yè)面一個(gè) HTTP 請(qǐng)求就能讓 Agent 拿到互聯(lián)網(wǎng)上的最新信息。但這里有個(gè)現(xiàn)實(shí)問(wèn)題Tavily 的 Key 要管大模型的 Key 也要管如果后面還要接別的模型或工具環(huán)境變量會(huì)越堆越多。我試過(guò)在三個(gè)不同的配置文件里來(lái)回切換 Key最后自己都搞混了。所以這篇內(nèi)容的核心思路是用 TaoToken 統(tǒng)一管理 Key 和 API 通道讓 Tavily 搜索工具通過(guò)一個(gè)穩(wěn)定的入口接入 Agent配置一次后面加工具、換模型都不用再動(dòng)環(huán)境變量。適合誰(shuí)看如果你正在用 Java 手寫(xiě)類 Manus 的 AI Agent已經(jīng)完成了基礎(chǔ)架構(gòu)和沙箱執(zhí)行現(xiàn)在想讓 Agent 能搜索互聯(lián)網(wǎng)這篇就是為你準(zhǔn)備的。我會(huì)給出完整的config.toml和settings.json骨架、TaoToken 接入 AI Agent 的配置片段以及一次搜索請(qǐng)求的驗(yàn)證動(dòng)作目標(biāo)只有一個(gè)讓 Agent 穩(wěn)定接入互聯(lián)網(wǎng)。2. TaoToken 前置統(tǒng)一 Key 與 API 通道在動(dòng)手改代碼之前先把 TaoToken 的接入準(zhǔn)備好。你可以把它理解成一個(gè)“Key 管家 通道調(diào)度器”Tavily 搜索、模型對(duì)話、代碼補(bǔ)全這些能力都通過(guò)同一個(gè) API 入口和同一套 Key 體系來(lái)調(diào)用。這樣做的好處是Agent 的配置文件里不需要散落各種廠商的 Key只需要維護(hù)一份 TaoToken 的憑證。2.1 獲取 TaoToken API Key打開(kāi) TaoToken 官網(wǎng)注冊(cè)或登錄后進(jìn)入控制臺(tái)在 API Keys 頁(yè)面創(chuàng)建一個(gè)新的 Key。建議按用途命名比如manus-agent-dev方便后面排查問(wèn)題時(shí)區(qū)分環(huán)境。創(chuàng)建完成后把 Key 復(fù)制出來(lái)它只會(huì)完整顯示一次。注意這個(gè) Key 不要直接寫(xiě)死在 Java 代碼里也不要提交到 Git 倉(cāng)庫(kù)。后面我們會(huì)用配置文件加環(huán)境變量的方式管理。2.2 確認(rèn) API 入口地址TaoToken 的 API 入口是https://taotoken.net/api所有通過(guò) TaoToken 轉(zhuǎn)發(fā)的請(qǐng)求都走這個(gè)地址。Tavily 搜索工具在底層發(fā)起 HTTP 請(qǐng)求時(shí)會(huì)把目標(biāo)指向這個(gè)入口由 TaoToken 完成后續(xù)的通道調(diào)度。你不需要在代碼里硬編碼 Tavily 官方的地址統(tǒng)一走 TaoToken 即可。2.3 規(guī)劃配置文件結(jié)構(gòu)在手寫(xiě) Manus 的項(xiàng)目里我建議把配置分成兩層一層是config.toml放 Agent 運(yùn)行時(shí)的全局參數(shù)另一層是settings.json放工具級(jí)別的開(kāi)關(guān)和參數(shù)。TaoToken 的 Key 和 API 地址放在config.toml的[llm]和[tools]段里Tavily 搜索的具體行為參數(shù)放在settings.json里。這樣后面加新工具時(shí)只需要在settings.json里加一段不用動(dòng)主配置。3. 可復(fù)制配置config.toml 與 settings.json 骨架下面這份配置可以直接復(fù)制到你的項(xiàng)目里按實(shí)際情況改 Key 和路徑即可。我盡量把注釋寫(xiě)清楚方便你對(duì)照自己的項(xiàng)目結(jié)構(gòu)調(diào)整。3.1 config.toml 骨架# config.toml - Agent 全局配置 [llm] # TaoToken 統(tǒng)一 API 入口 base_url https://taotoken.net/api # 從環(huán)境變量讀取避免硬編碼 api_key ${TAOTOKEN_API_KEY} # 模型名稱按需替換 model claude-3-5-sonnet timeout_seconds 60 [tools] # 工具總開(kāi)關(guān) enabled [file, sandbox, tavily_search] [tools.tavily_search] # 搜索工具也走 TaoToken 統(tǒng)一通道 base_url https://taotoken.net/api api_key ${TAOTOKEN_API_KEY} # 單次搜索返回結(jié)果數(shù)上限 max_results 5 # 請(qǐng)求超時(shí)單位秒 timeout_seconds 30 [agent] workspace ./workspace max_iterations 10這里的關(guān)鍵點(diǎn)是[tools.tavily_search]段base_url和api_key都指向 TaoToken而不是 Tavily 官方地址。這樣 Tavily 搜索工具在發(fā)起請(qǐng)求時(shí)實(shí)際是向 TaoToken 的 API 入口發(fā)送請(qǐng)求由 TaoToken 完成后續(xù)處理。3.2 settings.json 骨架{ tavily_search: { enabled: true, search_depth: basic, include_answer: false, include_raw_content: false, max_results: 5, search_params: { query: , topic: general } }, agent_runtime: { log_level: INFO, tool_call_timeout_ms: 30000, retry_on_failure: true, max_retries: 2 } }settings.json里放的是工具的行為參數(shù)比如搜索深度、是否包含原始內(nèi)容、重試策略。這些參數(shù)和 Key 無(wú)關(guān)所以單獨(dú)抽出來(lái)方便不同環(huán)境用不同的 JSON 文件覆蓋。3.3 環(huán)境變量注入在啟動(dòng) Agent 之前把 TaoToken 的 Key 注入環(huán)境變量export TAOTOKEN_API_KEY你的_TaoToken_API_Key如果你在 IDE 里跑可以在 Run Configuration 里加環(huán)境變量如果打包成 jar 跑用-D參數(shù)或者啟動(dòng)腳本里 export。這樣config.toml里的${TAOTOKEN_API_KEY}就能被正確替換。4. Java 側(cè)接入TavilySearchTool 改造配置準(zhǔn)備好之后回到 Java 代碼。原來(lái)的TavilySearchTool是直接從環(huán)境變量讀 Tavily 的 Key現(xiàn)在要改成從config.toml讀取 TaoToken 的配置。4.1 新增依賴除了原有的 langchain4j Tavily 封裝還需要一個(gè) TOML 解析庫(kù)來(lái)讀config.toml!-- Tavily 搜索引擎封裝 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-web-search-engine-tavily/artifactId version0.36.2/version /dependency !-- TOML 配置解析 -- dependency groupIdcom.moandjiezana.toml/groupId artifactIdtoml4j/artifactId version0.7.2/version /dependency4.2 讀取 TaoToken 配置寫(xiě)一個(gè)簡(jiǎn)單的配置加載類把config.toml里的[tools.tavily_search]段讀出來(lái)import com.moandjiezana.toml.Toml; import java.io.File; public class AgentConfigLoader { private final Toml toml; public AgentConfigLoader(String configPath) { this.toml new Toml().read(new File(configPath)); } public String getTavilyBaseUrl() { return toml.getString(tools.tavily_search.base_url); } public String getTavilyApiKey() { String key toml.getString(tools.tavily_search.api_key); // 支持 ${ENV_VAR} 形式的環(huán)境變量替換 if (key ! null key.startsWith(${) key.endsWith(})) { String envName key.substring(2, key.length() - 1); return System.getenv(envName); } return key; } public int getTavilyMaxResults() { Long value toml.getLong(tools.tavily_search.max_results); return value ! null ? value.intValue() : 5; } }4.3 改造 TavilySearchTool原來(lái)的工具類從System.getenv(TAVILY_API_KEY)讀 Key現(xiàn)在改成從配置加載器讀public class TavilySearchTool extends BaseTool { private final WebSearchEngine searchEngine; private final int defaultMaxResults; public TavilySearchTool(AgentConfigLoader config) { super(tavily_search, Search the web for real-time information); String apiKey config.getTavilyApiKey(); if (apiKey null || apiKey.trim().isEmpty()) { throw new IllegalStateException(TaoToken API Key is required for tavily_search); } this.defaultMaxResults config.getTavilyMaxResults(); // 關(guān)鍵baseUrl 指向 TaoToken 統(tǒng)一入口 this.searchEngine TavilyWebSearchEngine.builder() .apiKey(apiKey) .baseUrl(config.getTavilyBaseUrl()) .build(); } Override public MapString, Object getParametersSchema() { MapString, MapString, Object properties new HashMap(); properties.put(query, stringParam(The search query to execute)); properties.put(max_results, intParam(Max results, default defaultMaxResults)); return buildSchema(properties, List.of(query)); } Override public ToolResult execute(MapString, Object parameters) { try { String query getString(parameters, query); if (query null || query.trim().isEmpty()) { return ToolResult.error(Query parameter is required); } int maxResults getInt(parameters, max_results, defaultMaxResults); WebSearchResults results searchEngine.search(query); MapString, Object response new HashMap(); response.put(query, query); response.put(total_results, results.results().size()); ListMapString, Object items results.results().stream() .limit(maxResults) .map(r - { MapString, Object item new HashMap(); item.put(title, r.title()); item.put(url, r.url()); item.put(snippet, r.snippet()); return item; }) .toList(); response.put(results, items); return ToolResult.success(response); } catch (Exception e) { return ToolResult.error(Search failed: e.getMessage()); } } }注意baseUrl這一行它把搜索請(qǐng)求的目標(biāo)指向了 TaoToken 的 API 入口。Tavily 的官方 SDK 支持自定義 baseUrl所以這里不需要改底層 HTTP 邏輯只需要在構(gòu)建時(shí)傳入 TaoToken 的地址即可。4.4 注冊(cè)到 Agent在ManusAgent的初始化流程里把配置加載器和工具注冊(cè)串起來(lái)public class ManusAgent { private final ToolCollection toolCollection; public ManusAgent(String configPath) { AgentConfigLoader config new AgentConfigLoader(configPath); this.toolCollection new ToolCollection(); // 注冊(cè)文件工具 toolCollection.addTool(new FileReadTool(config)); toolCollection.addTool(new FileWriteTool(config)); // 注冊(cè)沙箱工具 toolCollection.addTool(new SandboxTool(config)); // 注冊(cè) Tavily 搜索工具走 TaoToken 統(tǒng)一通道 toolCollection.addTool(new TavilySearchTool(config)); } }到這里Java 側(cè)的改造就完成了。Tavily 搜索工具不再依賴單獨(dú)的 Tavily Key而是復(fù)用 TaoToken 的 Key 和 API 入口。5. 驗(yàn)證請(qǐng)求一次搜索的成功結(jié)果配置和代碼都改完之后先別急著跑完整的 Agent 流程單獨(dú)驗(yàn)證一次搜索請(qǐng)求確認(rèn) TaoToken 通道是通的。5.1 寫(xiě)一個(gè)最小驗(yàn)證類public class TavilySearchVerify { public static void main(String[] args) { AgentConfigLoader config new AgentConfigLoader(config.toml); TavilySearchTool tool new TavilySearchTool(config); MapString, Object params new HashMap(); params.put(query, Java AI Agent 開(kāi)源項(xiàng)目 2025); params.put(max_results, 3); ToolResult result tool.execute(params); if (result.isSuccess()) { System.out.println(搜索成功結(jié)果數(shù): result.getData().get(total_results)); ListMapString, Object items (ListMapString, Object) result.getData().get(results); for (MapString, Object item : items) { System.out.println(標(biāo)題: item.get(title)); System.out.println(鏈接: item.get(url)); System.out.println(摘要: item.get(snippet)); System.out.println(---); } } else { System.err.println(搜索失敗: result.getErrorMessage()); } } }5.2 預(yù)期輸出運(yùn)行這個(gè)類如果配置正確你會(huì)看到類似下面的輸出搜索成功結(jié)果數(shù): 3 標(biāo)題: 2025年值得關(guān)注的Java AI Agent框架 鏈接: https://example.com/java-ai-agent-2025 摘要: 本文整理了當(dāng)前主流的Java AI Agent開(kāi)源項(xiàng)目... --- 標(biāo)題: 手寫(xiě)Manus系列教程 鏈接: https://example.com/manus-java-tutorial 摘要: 從零用Java實(shí)現(xiàn)類Manus智能體涵蓋架構(gòu)、沙箱、搜索... --- 標(biāo)題: LangChain4j 最新進(jìn)展 鏈接: https://example.com/langchain4j-update 摘要: LangChain4j 近期新增了多個(gè)工具集成... ---5.3 接入 Agent 后的完整流程單獨(dú)驗(yàn)證通過(guò)后把搜索工具放進(jìn) Agent 的決策鏈里跑一次。用戶輸入“搜索一下最近有哪些新的 Java AI Agent 項(xiàng)目把結(jié)果寫(xiě)到文件里”Agent 的執(zhí)行流程會(huì)是這樣第一步大模型推理后調(diào)用tavily_searchquery 是“Java AI Agent 新項(xiàng)目 2025”TaoToken 通道返回結(jié)構(gòu)化搜索結(jié)果。第二步大模型從搜索結(jié)果里提取關(guān)鍵信息調(diào)用write_file把整理后的內(nèi)容寫(xiě)入workspace/java_agent_projects.txt。第三步大模型判斷任務(wù)完成返回finish_reasonstop。搜索結(jié)果被封裝成toolMessage存入 Memory 后下一輪推理時(shí)大模型能看到完整的搜索內(nèi)容并據(jù)此決定是繼續(xù)搜索、提取信息還是寫(xiě)入文件。這就是搜索工具流入 Agent 決策鏈的完整路徑。6. 本篇常見(jiàn)錯(cuò)排查配置和代碼都給了但實(shí)際跑的時(shí)候大概率會(huì)遇到幾個(gè)坑。下面這幾個(gè)是我在調(diào)試時(shí)踩過(guò)的按順序排查基本能覆蓋大部分問(wèn)題。6.1 搜索請(qǐng)求返回 401 或 403先檢查config.toml里的api_key是否被正確替換成了環(huán)境變量的值。可以在AgentConfigLoader里加一行日志把讀到的 Key 前幾位打印出來(lái)確認(rèn)。如果 Key 本身沒(méi)問(wèn)題檢查base_url是否寫(xiě)成了https://taotoken.net/api注意末尾不要多加斜杠也不要寫(xiě)成其他路徑。6.2 搜索結(jié)果為空或 total_results 為 0這種情況通常是 query 參數(shù)沒(méi)傳進(jìn)去或者max_results被設(shè)成了 0。檢查execute方法里getString(parameters, query)的返回值確認(rèn)大模型調(diào)用工具時(shí)確實(shí)填了 query 字段。另外settings.json里的max_results和config.toml里的max_results如果沖突以代碼里實(shí)際讀取的為準(zhǔn)建議只保留一處配置。6.3 工具注冊(cè)后 Agent 不調(diào)用如果 Agent 在應(yīng)該搜索的時(shí)候沒(méi)有調(diào)用tavily_search先檢查工具是否真的注冊(cè)進(jìn)了ToolCollection。可以在ManusAgent構(gòu)造完成后打印一下toolCollection里的工具名稱列表。另外工具的description要寫(xiě)清楚用途大模型是根據(jù)描述來(lái)決定調(diào)不調(diào)用的。Search the web for real-time information這種描述比search tool更容易被正確觸發(fā)。6.4 超時(shí)或連接失敗TaoToken 的 API 入口是 HTTPS確認(rèn)你的運(yùn)行環(huán)境能正常訪問(wèn)外網(wǎng)。如果公司網(wǎng)絡(luò)有代理需要在 JVM 啟動(dòng)參數(shù)里配置代理設(shè)置。另外config.toml里的timeout_seconds如果設(shè)得太短搜索請(qǐng)求可能還沒(méi)返回就被中斷了建議先設(shè) 30 秒測(cè)試。6.5 環(huán)境變量沒(méi)生效${TAOTOKEN_API_KEY}這種寫(xiě)法依賴AgentConfigLoader里的替換邏輯。如果你用的是其他 TOML 庫(kù)可能不支持這種語(yǔ)法需要手動(dòng)讀取環(huán)境變量再 set 進(jìn)去。最簡(jiǎn)單的驗(yàn)證方式是在main方法里直接System.out.println(System.getenv(TAOTOKEN_API_KEY))確認(rèn)環(huán)境變量在當(dāng)前進(jìn)程里可見(jiàn)。排查完這幾個(gè)點(diǎn)Tavily 搜索工具基本就能穩(wěn)定工作了。后面如果要加新的搜索源或者換模型只需要在 TaoToken 控制臺(tái)調(diào)整通道配置Java 側(cè)不用改代碼。7. 接入文檔與后續(xù)工具擴(kuò)展Tavily 搜索工具跑通之后你的 Agent 工具箱里就有了文件讀寫(xiě)、沙箱執(zhí)行、網(wǎng)頁(yè)搜索三類能力。這三者組合起來(lái)能做的事情比單個(gè)工具疊加要多得多搜索獲取實(shí)時(shí)信息沙箱里跑代碼處理數(shù)據(jù)最后把結(jié)果寫(xiě)入文件。如果你在接入過(guò)程中遇到 Key 配置或通道相關(guān)的問(wèn)題可以直接看 TaoToken 的接入文檔里面有各語(yǔ)言的最小接入示例和常見(jiàn)錯(cuò)誤碼說(shuō)明。需要管理多個(gè) Key 或者查看調(diào)用量的話控制臺(tái)里有按工具維度的統(tǒng)計(jì)。后續(xù)如果要給 Agent 加新的工具比如代碼補(bǔ)全或者長(zhǎng)文本摘要建議繼續(xù)走 TaoToken 的統(tǒng)一通道這樣配置文件里只需要加一段[tools.xxx]不用再引入新的 Key 管理體系。搜索工具只是 Agent 接入互聯(lián)網(wǎng)的第一步。真正讓 Agent 變得好用的是工具之間的組合調(diào)用而組合調(diào)用的前提是每個(gè)工具都能穩(wěn)定、可配置地工作。把 TaoToken 作為統(tǒng)一入口后面加工具、換模型、調(diào)參數(shù)都會(huì)輕松很多。