Tools
Tools & ExecutionOne Handler Per Tool
你跟 Claude Code 说一件事,它自己挑该用哪个工具——读文件用 Read、跑 Stata 用 mcp__stata-mcp__stata_do、跨文件搜词用 Grep、写审计报告用 Write。代码这边只是按工具名字找到对应的执行函数,调用一下,把结果回传。挑哪个工具完全是模型的判断。
要核对 04_中间数据/main_panel.dta 里 PC(A2 主口径)的字段定义和 02_变量字典/测算方法说明.md 的口径是否一致,再确认 do6_baseline.do 里 reghdfe 的 PC 解释变量名跟 panel 数据集对得上。一句指令要走读 .md → 调 stata-mcp 看变量 label → grep 找代码用法 → 写一份核对报告四步,每步用不同工具。
s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12
"Adding a tool means adding one handler" -- the loop stays the same; new tools register into the dispatch map.
Harness layer: Tool dispatch -- expanding what the model can reach.
Problem
With only bash, the agent shells out for everything. cat truncates unpredictably, sed fails on special characters, and every bash call is an unconstrained security surface. Dedicated tools like read_file and write_file let you enforce path sandboxing at the tool level.
The key insight: adding tools does not require changing the loop.
Solution
+--------+ +-------+ +------------------+
| User | ---> | LLM | ---> | Tool Dispatch |
| prompt | | | | { |
+--------+ +---+---+ | bash: run_bash |
^ | read: run_read |
| | write: run_wr |
+-----------+ edit: run_edit |
tool_result | } |
+------------------+
The dispatch map is a dict: {tool_name: handler_function}.
One lookup replaces any if/elif chain.
How It Works
- Each tool gets a handler function. Path sandboxing prevents workspace escape.
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
def run_read(path: str, limit: int = None) -> str:
text = safe_path(path).read_text()
lines = text.splitlines()
if limit and limit < len(lines):
lines = lines[:limit]
return "\n".join(lines)[:50000]
- The dispatch map links tool names to handlers.
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"],
kw["new_text"]),
}
- In the loop, look up the handler by name. The loop body itself is unchanged from s01.
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler \
else f"Unknown tool: {block.name}"
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
Add a tool = add a handler + add a schema entry. The loop never changes.
What Changed From s01
| Component | Before (s01) | After (s02) |
|---|---|---|
| Tools | 1 (bash only) | 4 (bash, read, write, edit) |
| Dispatch | Hardcoded bash call | TOOL_HANDLERS dict |
| Path safety | None | safe_path() sandbox |
| Agent loop | Unchanged | Unchanged |
Try It
cd claude-code-for-researchers
python agents/s02_tool_use.py
Read 02_变量字典/测算方法说明.md to understand the A2 frameworkRun stata-mcp summarize PC_A2 ESG controls on main_panel.dtaGrep '\\cite{' across 07_论文写作/ and 01_文献/_按研究主题索引.mdWrite audit-variable-dict.md listing fields in main_panel.dta that have no entry in 02_变量字典/