工具箱、专家模型与人类协作
30 秒导读: 光有会思考的大脑不够,agent 还得有「手脚」去真正改文件、跑命令、搜代码。本章盘点 RA.Aid 给 agent 装了哪些手脚(动作类 / 检索类 / 研究类工具),以及当 agent 自己搞不定时的两条 特殊求助通道——把难题连同上下文上交给更强的推理模型(
ask_expert),或者干脆把决定权交回给 人(ask_human)。最后看这些工具如何按「研究→规划→实现」的阶段被动态拼装。
本章聚焦「工具本身解决什么问题、每个取舍怎么选」。工具的输出如何存成 trajectory / 记忆见 03 记忆与持久化;工具调用字符串如何被 CIAYN 后端 执行见 02 两种 agent 后端。
1. 这是什么(零基础也能懂)
一个 LLM 本身只会「说话」——输入文字、输出文字。它没法自己打开文件、跑测试、上网查资料。
工具(tool)就是给这张「嘴」接上的手脚: 模型说「我要读 foo.py」,工具真的去磁盘读回来。
RA.Aid 的每个工具都是一个用 @tool 装饰的 Python 函数(来自 langchain_core.tools),模型通过
生成对该函数的调用来「动手」。按「解决什么问题」,这些工具大致分三类,外加两条应急通道:
| 类别 | 干什么 | 代表工具 |
|---|---|---|
| 动作类 | 改真实世界:跑命令、写/改文件 | run_shell_command、run_programming_task、file_str_replace |
| 检索类 | 在代码库里找东西 | ripgrep_search、fuzzy_find_project_files、read_file_tool |
| 研究类 | 到代码库外找信息 | web_search_tavily、emit_research_notes |
| 专家通道 | 自己想不通时,上交更强的模型 | emit_expert_context + ask_expert |
| 人类在环 | 需要人拍板时,把问题抛给人 | ask_human |
一句话直觉: 把 agent 想成一个新来的实习生。检索工具是它的「眼睛」(看代码),动作工具是它的 「手 」(改代码),专家通道是它「问资深同事」,人类在环是它「问项目负责人」。
用起来什么样: 你在终端里让 agent 「给这个函数加上错误处理」,它内部可能依次调用
ripgrep_search 找到函数 → read_file_tool 读上下文 → file_str_replace 改代码 →(拿不准时)
ask_expert 让 o1 复核逻辑。你在屏幕上看到的是一连串带图标的面板(🔎/📄/✓/🤔)。
2. 工具全景(它大概怎么转)
怎么读这张图: 中间是 agent 的推理循环,四周是它能伸出去的「手脚」;实线是每步都可能用的常规 工具,虚线是两条「求助」通道。方向是「agent 主动调用 → 工具作用于某个目标 → 结果回到 agent」。
┌────────────────────────────┐
检索/研究(眼睛) │ │ 动作(手)
ripgrep_search ──────▶│ │──────▶ run_shell_command
fuzzy_find ───────────▶│ agent 推理循环 │──────▶ run_programming_task(aider)
read_file_tool ───────▶│ (ReAct / CIAYN 后端) │──────▶ file_str_replace
web_search_tavily ────▶│ │──────▶ put_complete_file_contents
│ │
└───────┬──────────────┬─────┘
┆ ┆
求助通道 A ┆ ┆ 求助通道 B
(更强的模型) ▼ ▼ (真人)
emit_expert_context ask_human
+ ask_expert (记入 human_input 表)
→ o1 等推理模型
部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
| 动作工具 | 落地改变:跑 shell、拼 aider 子进程、精确替换字符串、整文件覆写 | tools/shell.py、tools/programmer.py、tools/file_str_replace.py、tools/write_file.py |
| 检索工具 | 找代码:正则搜索、模糊找文件名、读整文件、列目录树 | tools/ripgrep.py、tools/fuzzy_find.py、tools/read_file.py、tools/list_directory.py |
| 研究工具 | 找外部信息:Tavily 网络搜索 | tools/web_search_tavily.py |
| 专家通道 | 把难题+上下文交给更强模型 | tools/expert.py、prompts/expert_prompts.py |
| 人类在环 | 把问题抛给真人,答案入库 | tools/human.py |
| 工具装配 | 按阶段选出该给哪批工具 | tool_configs.py |
主线走一遍(高层):agent 后端拿到的工具清单不是固定的,而是 tool_configs.py 按当前阶段
(研究 / 规划 / 实现 / 聊天)现拼的一份子集。研究阶段基本只给「只读 + 求助」工具,实现阶段才追加
「改文件」工具——用工具清单本身来约束 agent 在每个阶段能做什么。
3. 动作类工具(agent 的「手」)
这是工程含量最高的一支。难点从来不是「让模型说要改什么」,而是「把模型说的那段话,精确、可靠地 落到真实文件/命令上」。RA.Aid 在这里有三组不同粒度的手段 。
3.1 run_shell_command —— 跑命令,和 cowboy_mode 审批开关
要解决的小问题: agent 经常需要跑命令(跑测试、git status、删文件)。但让一个 AI 随手在你机器
上执行任意命令是危险的——所以默认每条命令执行前都要人点头。
思路: 默认弹一个三选一的确认框,cowboy_mode 则是「一路放行」的逃生舱。看真实实现:
# tools/shell.py:65 run_shell_command
cowboy_mode = get_config_repository().get("cowboy_mode", False)
# ... 展示命令面板后 ...
if not cowboy_mode: # shell.py:89
response = Prompt.ask(
"Execute this command? (y=yes, n=no, c=enable cowboy mode for session)",
choices=["y", "n", "c"], default="y", ...
)
if response == "n":
return {"output": "Command execution cancelled by user", ...}
elif response == "c": # shell.py:106
get_config_repository().set("cowboy_mode", True) # 本 session 之后都不再问
三个选项的语义,以及背后的取舍:
| 输入 | 行为 | 取舍 |
|---|---|---|
y | 执行这一条 | 安全但每条都要盯着,慢 |
n | 拒绝,返回 "cancelled by user" | agent 收到失败信号,会另想办法 |
c | 打开 cowboy_mode,本 session 之后不再询问 | 全自动、快,但把审批全交出去了 |
关键细节: cowboy_mode 是写进 config repository 的会话级开关,一旦打开当场生效、后续每条命令
直接跑(shell.py:67 还会打印一句牛仔风格的调侃)。命令通过 _detect_shell()(shell.py:21)
选 bash / powershell,再交给 run_interactive_command 执行;输出经 truncate_output 裁到 5000 行
以内(text/processing.py:7)才回给模型,防止刷屏把上下文撑爆。
3.2 run_programming_task —— 把写代码外包给 aider(--use-aider)
要解决的小问题: 让 LLM 直接逐字重写整个文件既费 token 又容易出错。RA.Aid 提供一个可选路径:
把「写代码」这件事外包给专门的编码工具 aider,即 README 里的 --use-aider。
思路: 这个工具本质是拼一条 aider 子进程命令行并执行。它把 agent 的指令、要碰的文件路径, 连同一串固定 flag 组装起来:
# tools/programmer.py:57 run_programming_task —— 拼命令
command = [
"aider", "--yes-always", "--no-git", "--no-auto-commits",
"--dark-mode", "--no-suggest-shell-commands",
"--no-show-release-notes", "--no-check-update",
]
# ... 合并 files 参数 + related_files(去重、转绝对路径)programmer.py:79 ...
if "AIDER_FLAGS" in os.environ: # programmer.py:89
command.extend(parse_aider_flags(os.environ["AIDER_FLAGS"]))
command.append("-m"); command.append(instructions) # programmer.py:97-99
几个巧妙约束(都写在 docstring 里,是给模型的合同):
- programmer「只看得到你给的东西,没有对话历史」——所以调用前必须先用
emit_related_files把相关文件登记进来,否则 aider 看不到(programmer.py:38-40)。 - 「只能增/改文件,不能删」——要删文件得用
run_shell_command(programmer.py:48)。 - 指令里不准写代码、控制在 300 词内(
programmer.py:42-44),逼 agent 给「意图」而非「实现 」。
parse_aider_flags(programmer.py:160)负责把 AIDER_FLAGS 环境变量里 yes-always,dark-mode
这类逗号串规整成 ['--yes-always', '--dark-mode'],并支持带值的 flag(--analytics-log file.json)。
这是一个取舍点: 用 aider = 借它成熟的代码编辑能力,代价是多一个外部依赖、多一层子进程。不用
aider(默认),就走下面 3.3 的原生文件工具。这个二选一由 set_modification_tools 控制(见 §7)。
3.3 原生文件工具 —— 精确替换 vs 整文件覆写
不走 aider 时,RA.Aid 用两个更轻的原生工具改文件,分别对应两种粒度:
| 工具 | 适合 | 关键安全阀 |
|---|---|---|
file_str_replace | 局部改一处 | 旧串必须唯一出现,否则拒绝 |
put_complete_file_contents | 整文件重写/新建 | 直接覆盖,目录自动创建 |
file_str_replace 的护栏值得看:它先数旧串出现几次,出现 0 次或多于 1 次(且没开
replace_all)都直接失败,不做任何修改——避免「改错地方」或「一次误改多处」。
# tools/file_str_replace.py:91 file_str_replace
count = content.count(old_str)
if count == 0: # 找不到 → 失败
return {"success": False, "message": f"String not found: ..."}
elif count > 1 and not replace_all: # file_str_replace.py:122 不唯一 → 失败
return {"success": False,
"message": f"String appears {count} times - must be unique ..."}
改完文件后两个工具都会调 emit_related_files(file_str_replace.py:197、write_file.py:86),
把刚碰过的文件自动登记进记忆,这样后续 expert / programmer 能看到它们——工具之间靠这条隐性纽带
共享上下文(记忆细节见 03)。
4. 检索与研究类工具(agent 的「眼睛」)
改代码之前得先看懂代码。这一组解决「在大项目里快速定位」的问题,并刻意省 token。
4.1 ripgrep_search —— 带排除项的正则搜索
要解决的小问题: 在几万文件的库里找一个符号,grep -r 会又慢又刷屏。
ripgrep_search(ripgrep.py:75)包装了 rg,并预置一串默认排除目录——.git、
node_modules、__pycache__、dist、.venv 等(ripgrep.py:17 DEFAULT_EXCLUDE_DIRS),
避免把依赖和缓存也搜进来。它的 docstring 明确建议:优先用 before/after_context_lines
带上下文行,而不是读整个文件,以省 token(ripgrep.py:91)。结果同样过 truncate_output
截断后才回给 agent(ripgrep.py:197)。
4.2 fuzzy_find_project_files —— 记不全文件名时的模糊匹配
要解决的小问题: agent 只记得文件名大概长啥样(「那个 config 相关的」),但打不全路径。
fuzzy_find_project_files(fuzzy_find.py:70)用 fuzzywuzzy 做近似匹配:列出项目全部文件后,
process.extract 打分,再按 threshold(默认 60)过滤(fuzzy_find.py:173-176)。返回
(路径, 分数) 列表,给 agent「你要找的可能是这几个」。