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