1. 为什么你的 Java Agent 还差一双“眼睛”
大模型开发走到手写 Manus 这一步,文件读写和 Docker 沙箱代码执行都已经跑通了,但你会发现 Agent 依然是个“闭卷考生”。你问它“今天有什么值得关注的 AI 开源项目”,它只能从训练数据里翻旧账,给出的答案可能停留在几个月前。这不是模型不够聪明,而是它缺少一个能实时访问互联网的搜索工具。
Tavily 就是为 AI Agent 量身定做的搜索 API。它不像传统搜索引擎那样返回一堆需要解析的 HTML,而是直接给你结构化的 JSON:标题、链接、摘要,拿来就能塞进大模型的上下文。对于 Java 手写 Manus 的场景来说,这意味着你不需要写爬虫、不需要处理反爬、不需要清洗页面,一个 HTTP 请求就能让 Agent 拿到互联网上的最新信息。
但这里有个现实问题:Tavily 的 Key 要管,大模型的 Key 也要管,如果后面还要接别的模型或工具,环境变量会越堆越多。我试过在三个不同的配置文件里来回切换 Key,最后自己都搞混了。所以这篇内容的核心思路是:用 TaoToken 统一管理 Key 和 API 通道,让 Tavily 搜索工具通过一个稳定的入口接入 Agent,配置一次,后面加工具、换模型都不用再动环境变量。
适合谁看?如果你正在用 Java 手写类 Manus 的 AI Agent,已经完成了基础架构和沙箱执行,现在想让 Agent 能搜索互联网,这篇就是为你准备的。我会给出完整的config.toml和settings.json骨架、TaoToken 接入 AI Agent 的配置片段,以及一次搜索请求的验证动作,目标只有一个:让 Agent 稳定接入互联网。
2. TaoToken 前置:统一 Key 与 API 通道
在动手改代码之前,先把 TaoToken 的接入准备好。你可以把它理解成一个“Key 管家 + 通道调度器”:Tavily 搜索、模型对话、代码补全这些能力,都通过同一个 API 入口和同一套 Key 体系来调用。这样做的好处是,Agent 的配置文件里不需要散落各种厂商的 Key,只需要维护一份 TaoToken 的凭证。
2.1 获取 TaoToken API Key
打开 TaoToken 官网,注册或登录后进入控制台,在 API Keys 页面创建一个新的 Key。建议按用途命名,比如manus-agent-dev,方便后面排查问题时区分环境。创建完成后把 Key 复制出来,它只会完整显示一次。
注意:这个 Key 不要直接写死在 Java 代码里,也不要提交到 Git 仓库。后面我们会用配置文件加环境变量的方式管理。
2.2 确认 API 入口地址
TaoToken 的 API 入口是https://taotoken.net/api,所有通过 TaoToken 转发的请求都走这个地址。Tavily 搜索工具在底层发起 HTTP 请求时,会把目标指向这个入口,由 TaoToken 完成后续的通道调度。你不需要在代码里硬编码 Tavily 官方的地址,统一走 TaoToken 即可。
2.3 规划配置文件结构
在手写 Manus 的项目里,我建议把配置分成两层:一层是config.toml,放 Agent 运行时的全局参数;另一层是settings.json,放工具级别的开关和参数。TaoToken 的 Key 和 API 地址放在config.toml的[llm]和[tools]段里,Tavily 搜索的具体行为参数放在settings.json里。这样后面加新工具时,只需要在settings.json里加一段,不用动主配置。
3. 可复制配置:config.toml 与 settings.json 骨架
下面这份配置可以直接复制到你的项目里,按实际情况改 Key 和路径即可。我尽量把注释写清楚,方便你对照自己的项目结构调整。
3.1 config.toml 骨架
# config.toml - Agent 全局配置 [llm] # TaoToken 统一 API 入口 base_url = "https://taotoken.net/api" # 从环境变量读取,避免硬编码 api_key = "${TAOTOKEN_API_KEY}" # 模型名称按需替换 model = "claude-3-5-sonnet" timeout_seconds = 60 [tools] # 工具总开关 enabled = ["file", "sandbox", "tavily_search"] [tools.tavily_search] # 搜索工具也走 TaoToken 统一通道 base_url = "https://taotoken.net/api" api_key = "${TAOTOKEN_API_KEY}" # 单次搜索返回结果数上限 max_results = 5 # 请求超时,单位秒 timeout_seconds = 30 [agent] workspace = "./workspace" max_iterations = 10这里的关键点是[tools.tavily_search]段:base_url和api_key都指向 TaoToken,而不是 Tavily 官方地址。这样 Tavily 搜索工具在发起请求时,实际是向 TaoToken 的 API 入口发送请求,由 TaoToken 完成后续处理。
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里放的是工具的行为参数,比如搜索深度、是否包含原始内容、重试策略。这些参数和 Key 无关,所以单独抽出来,方便不同环境用不同的 JSON 文件覆盖。
3.3 环境变量注入
在启动 Agent 之前,把 TaoToken 的 Key 注入环境变量:
export TAOTOKEN_API_KEY="你的_TaoToken_API_Key"如果你在 IDE 里跑,可以在 Run Configuration 里加环境变量;如果打包成 jar 跑,用-D参数或者启动脚本里 export。这样config.toml里的${TAOTOKEN_API_KEY}就能被正确替换。
4. Java 侧接入:TavilySearchTool 改造
配置准备好之后,回到 Java 代码。原来的TavilySearchTool是直接从环境变量读 Tavily 的 Key,现在要改成从config.toml读取 TaoToken 的配置。
4.1 新增依赖
除了原有的 langchain4j Tavily 封装,还需要一个 TOML 解析库来读config.toml:
<!-- Tavily 搜索引擎封装 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-web-search-engine-tavily</artifactId> <version>0.36.2</version> </dependency> <!-- TOML 配置解析 --> <dependency> <groupId>com.moandjiezana.toml</groupId> <artifactId>toml4j</artifactId> <version>0.7.2</version> </dependency>4.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} 形式的环境变量替换 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,现在改成从配置加载器读:
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 统一入口 this.searchEngine = TavilyWebSearchEngine.builder() .apiKey(apiKey) .baseUrl(config.getTavilyBaseUrl()) .build(); } @Override public Map<String, Object> getParametersSchema() { Map<String, Map<String, 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(Map<String, 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); Map<String, Object> response = new HashMap<>(); response.put("query", query); response.put("total_results", results.results().size()); List<Map<String, Object>> items = results.results().stream() .limit(maxResults) .map(r -> { Map<String, 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 统一通道 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); Map<String, 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("搜索成功,结果数: " + result.getData().get("total_results")); List<Map<String, Object>> items = (List<Map<String, Object>>) result.getData().get("results"); for (Map<String, 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 预期输出
运行这个类,如果配置正确,你会看到类似下面的输出:
搜索成功,结果数: 3 标题: 2025年值得关注的Java AI Agent框架 链接: https://example.com/java-ai-agent-2025 摘要: 本文整理了当前主流的Java AI Agent开源项目... --- 标题: 手写Manus系列教程 链接: https://example.com/manus-java-tutorial 摘要: 从零用Java实现类Manus智能体,涵盖架构、沙箱、搜索... --- 标题: LangChain4j 最新进展 链接: https://example.com/langchain4j-update 摘要: LangChain4j 近期新增了多个工具集成... ---5.3 接入 Agent 后的完整流程
单独验证通过后,把搜索工具放进 Agent 的决策链里跑一次。用户输入“搜索一下最近有哪些新的 Java AI Agent 项目,把结果写到文件里”,Agent 的执行流程会是这样:
第一步,大模型推理后调用tavily_search,query 是“Java AI Agent 新项目 2025”,TaoToken 通道返回结构化搜索结果。第二步,大模型从搜索结果里提取关键信息,调用write_file把整理后的内容写入workspace/java_agent_projects.txt。第三步,大模型判断任务完成,返回finish_reason="stop"。
搜索结果被封装成toolMessage存入 Memory 后,下一轮推理时大模型能看到完整的搜索内容,并据此决定是继续搜索、提取信息还是写入文件。这就是搜索工具流入 Agent 决策链的完整路径。
6. 本篇常见错排查
配置和代码都给了,但实际跑的时候大概率会遇到几个坑。下面这几个是我在调试时踩过的,按顺序排查基本能覆盖大部分问题。
6.1 搜索请求返回 401 或 403
先检查config.toml里的api_key是否被正确替换成了环境变量的值。可以在AgentConfigLoader里加一行日志,把读到的 Key 前几位打印出来确认。如果 Key 本身没问题,检查base_url是否写成了https://taotoken.net/api,注意末尾不要多加斜杠,也不要写成其他路径。
6.2 搜索结果为空或 total_results 为 0
这种情况通常是 query 参数没传进去,或者max_results被设成了 0。检查execute方法里getString(parameters, "query")的返回值,确认大模型调用工具时确实填了 query 字段。另外,settings.json里的max_results和config.toml里的max_results如果冲突,以代码里实际读取的为准,建议只保留一处配置。
6.3 工具注册后 Agent 不调用
如果 Agent 在应该搜索的时候没有调用tavily_search,先检查工具是否真的注册进了ToolCollection。可以在ManusAgent构造完成后打印一下toolCollection里的工具名称列表。另外,工具的description要写清楚用途,大模型是根据描述来决定调不调用的。"Search the web for real-time information"这种描述比"search tool"更容易被正确触发。
6.4 超时或连接失败
TaoToken 的 API 入口是 HTTPS,确认你的运行环境能正常访问外网。如果公司网络有代理,需要在 JVM 启动参数里配置代理设置。另外,config.toml里的timeout_seconds如果设得太短,搜索请求可能还没返回就被中断了,建议先设 30 秒测试。
6.5 环境变量没生效
${TAOTOKEN_API_KEY}这种写法依赖AgentConfigLoader里的替换逻辑。如果你用的是其他 TOML 库,可能不支持这种语法,需要手动读取环境变量再 set 进去。最简单的验证方式是在main方法里直接System.out.println(System.getenv("TAOTOKEN_API_KEY")),确认环境变量在当前进程里可见。
排查完这几个点,Tavily 搜索工具基本就能稳定工作了。后面如果要加新的搜索源或者换模型,只需要在 TaoToken 控制台调整通道配置,Java 侧不用改代码。
7. 接入文档与后续工具扩展
Tavily 搜索工具跑通之后,你的 Agent 工具箱里就有了文件读写、沙箱执行、网页搜索三类能力。这三者组合起来,能做的事情比单个工具叠加要多得多:搜索获取实时信息,沙箱里跑代码处理数据,最后把结果写入文件。
如果你在接入过程中遇到 Key 配置或通道相关的问题,可以直接看 TaoToken 的接入文档,里面有各语言的最小接入示例和常见错误码说明。需要管理多个 Key 或者查看调用量的话,控制台里有按工具维度的统计。后续如果要给 Agent 加新的工具,比如代码补全或者长文本摘要,建议继续走 TaoToken 的统一通道,这样配置文件里只需要加一段[tools.xxx],不用再引入新的 Key 管理体系。
搜索工具只是 Agent 接入互联网的第一步。真正让 Agent 变得好用的是工具之间的组合调用,而组合调用的前提是每个工具都能稳定、可配置地工作。把 TaoToken 作为统一入口,后面加工具、换模型、调参数都会轻松很多。