I Built an AI Agent with Claude's Tool-Use Loop (Web Search, SQL, and More)

I Built an AI Agent with Claude’s Tool-Use Loop (Web Search, SQL, and More)

我利用 Claude 的工具调用循环构建了一个 AI Agent(支持网页搜索、SQL 等)

“AI agent” gets thrown around so much I figured I should just build one instead of reading more threads about it. The core idea turned out to be small: you put Claude in a loop and hand it some tools. It picks a tool, you run it, you hand back the result, and it keeps going until it has an answer. Code is here if you want to skip ahead: claude-research-agent. “AI agent”(人工智能代理)这个词被滥用到我决定不再阅读相关讨论,而是直接动手做一个。其核心理念其实很简单:让 Claude 进入一个循环,并为它提供一些工具。它选择一个工具,你运行它,将结果反馈给它,它会持续运行直到得出答案。如果你想直接看代码,请点击这里:claude-research-agent。

What it can do

它能做什么

Give it a task and it works out the steps on its own. Mine can: 给它一个任务,它能自行规划步骤。我的 Agent 可以:

  • search the web (no API key for this part, it just hits DuckDuckGo’s HTML page)
  • 搜索网页(此部分无需 API 密钥,直接抓取 DuckDuckGo 的 HTML 页面)
  • open a URL and pull the readable text out
  • 打开 URL 并提取可读文本
  • do math without me trusting eval
  • 进行数学运算(无需信任 eval 函数)
  • run read-only SQL against a SQLite file
  • 对 SQLite 文件执行只读 SQL 查询
  • read local files, but only inside the project folder
  • 读取本地文件(仅限项目文件夹内)
  • save findings to a notes file
  • 将发现保存到笔记文件

The loop is basically the whole thing

循环是整个系统的核心

Honestly this is most of it: 老实说,核心逻辑就在这里:

messages = [{"role": "user", "content": user_message}]
for _ in range(MAX_STEPS):
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=2048,
        tools=TOOL_SCHEMAS,
        messages=messages,
    )
    messages.append({"role": "assistant", "content": response.content})
    if response.stop_reason != "tool_use":
        return final_text(response)
    
    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            result = run_tool(block.name, block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result,
            })
    messages.append({"role": "user", "content": tool_results})

The thing to watch is stop_reason. If Claude says tool_use, it wants you to run something. You run it, drop the result back into the conversation as a tool_result, and loop. When it stops asking for tools, you’re done. The MAX_STEPS cap is just there so a confused agent can’t spin forever. 需要关注的是 stop_reason。如果 Claude 返回 tool_use,说明它需要你运行某个工具。你运行它,将结果作为 tool_result 放回对话中,然后继续循环。当它不再请求工具时,任务就完成了。设置 MAX_STEPS 上限只是为了防止 Agent 进入死循环。

Tools are just functions

工具本质上就是函数

Each tool is a Python function plus a little JSON schema telling Claude when to reach for it. Want a new capability? Write a function, add its schema. The loop never changes, which was the part I liked most. 每个工具都是一个 Python 函数,外加一段告诉 Claude 何时调用它的 JSON 模式(Schema)。想要新功能?写个函数,添加对应的模式即可。循环逻辑永远不需要改变,这是我最喜欢的部分。

Guardrails, because this stuff can bite

安全防护,因为这东西可能会反噬

The second your agent can run code or touch a database, you have to be careful: 一旦你的 Agent 可以运行代码或操作数据库,你就必须小心:

  • the calculator parses an AST and only allows math, so something like __import__("os")... gets rejected instead of run
  • 计算器会解析 AST(抽象语法树)并仅允许数学运算,因此像 __import__("os")... 这样的代码会被拒绝执行
  • SQL is opened read-only and I block write keywords on top of that
  • SQL 以只读方式打开,并且我还在其基础上屏蔽了写入关键字
  • file reads can’t escape the project folder, so no ../../etc/passwd
  • 文件读取无法跳出项目文件夹,所以不用担心 ../../etc/passwd Skip this and you’ve basically built a foot-gun with a chat interface. 如果不做这些防护,你基本上就是造了一个带聊天界面的“自残工具”。

Seeing it actually run

实际运行效果

Here’s a real run. I asked it to look up the latest Claude model and do some math, and it chained five tool calls by itself: 这是一次真实的运行记录。我让它查找最新的 Claude 模型并进行一些数学计算,它自动串联了五个工具调用:

you> Search for the latest Claude model, then calculate 15% of 2340. web_search({"query": "latest Claude model"}) calculator({"expression": "2340 * 0.15"}) fetch_url({"url": "https://www.anthropic.com/news"}) save_note({...}) agent> 15% of 2340 = 351, plus a short rundown of the current models.

Watching it decide the order on its own is the part that finally made “agent” click for me. 看着它自主决定执行顺序,这让我终于真正理解了什么是“Agent”。

The bug that ate an hour

耗费一小时的 Bug

My web search kept coming back empty even though the results were clearly in the HTML. Drove me a little crazy. Turns out DuckDuckGo wraps the matched words in <b> tags inside each snippet. My parser was resetting its state on every closing tag, so the inner </b> wiped everything before the result ever got saved. The fix was to only reset on the closing </a> and flush the last result at the end. Classic “the real-world HTML is messier than your test case” moment. 我的网页搜索结果一直为空,尽管 HTML 里明明有内容。这让我快疯了。结果发现 DuckDuckGo 在每个片段的匹配词周围包裹了 <b> 标签。我的解析器在每个闭合标签处都会重置状态,导致内部的 </b> 在结果保存前就清空了一切。修复方法是仅在 </a> 闭合时重置,并在最后刷新结果。这是典型的“现实世界的 HTML 比你的测试用例更混乱”的时刻。

If you want to poke at it

如果你想尝试一下

The repo has the CLI, a small FastAPI web UI that shows the tool trace, and a test suite: github.com/venkatarahul27/claude-research-agent. Curious what tools other people would bolt on. I’m tempted to give it a real browser next. 仓库里包含了 CLI、一个展示工具调用轨迹的 FastAPI Web UI 以及测试套件:github.com/venkatarahul27/claude-research-agent。很好奇其他人会给它添加什么工具。我打算下一步给它接入一个真正的浏览器。