跳到主要内容

工具箱、专家模型与人类协作

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_commandrun_programming_taskfile_str_replace
检索类在代码库里找东西ripgrep_searchfuzzy_find_project_filesread_file_tool
研究类到代码库外找信息web_search_tavilyemit_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.pytools/programmer.pytools/file_str_replace.pytools/write_file.py
检索工具找代码:正则搜索、模糊找文件名、读整文件、列目录树tools/ripgrep.pytools/fuzzy_find.pytools/read_file.pytools/list_directory.py
研究工具找外部信息:Tavily 网络搜索tools/web_search_tavily.py
专家通道把难题+上下文交给更强模型tools/expert.pyprompts/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:197write_file.py:86), 把刚碰过的文件自动登记进记忆,这样后续 expert / programmer 能看到它们——工具之间靠这条隐性纽带 共享上下文(记忆细节见 03)。


4. 检索与研究类工具(agent 的「眼睛」)

改代码之前得先看懂代码。这一组解决「在大项目里快速定位」的问题,并刻意省 token

4.1 ripgrep_search —— 带排除项的正则搜索

要解决的小问题: 在几万文件的库里找一个符号,grep -r 会又慢又刷屏。

ripgrep_search(ripgrep.py:75)包装了 rg,并预置一串默认排除目录——.gitnode_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「你要找的可能是这几个」。

4.3 read_file_tool / list_directory_tree —— 读文件与看结构,都有护栏

read_file_tool(read_file.py:59)读整个文本文件,但有两道护栏:拒读二进制文件 (read_file.py:92is_binary_file 检测,命中就返回错误而非乱码),以及读完同样过 truncate_output 裁到 5000 行(read_file.py:166)——这就是任务里说的「read_file 的行限制」: 不是按参数限行,而是统一用 5000 行上限保护上下文窗口。

list_directory_tree(list_directory.py:179)渲染目录树,默认 max_depth=1(不递归) (list_directory.py:183),并自动读 .gitignore / .aiderignore 跳过被忽略的文件——同样是 「默认少输出」的一贯设计。

4.4 web_search_tavily —— 唯一的对外通道

前面都是「向内看代码库」;web_search_tavily(web_search_tavily.py:17)是唯一向外的工具, 用 Tavily API 做网络搜索,需要 TAVILY_API_KEY(web_search_tavily.py:45)。它解决的是 expert 模型解决不了的一类问题——「查最新信息」,这一点在 expert 的 prompt 里被明确点出(见 §5)。


5. 专家升级通道:把难题上交更强的模型

要解决的小问题: 主 agent 用的模型要兼顾速度和成本,遇到烧脑的逻辑/调试题可能力不从心。 RA.Aid 的办法是准备一条升级通道:把难题连同全部上下文,一次性交给一个更强、更贵的推理模型 (如 OpenAI o1),让它专心想一道题。

5.1 两步走:先攒上下文,再提问

这条通道是两个工具配合的:

emit_expert_context(context) # 步骤1:塞进详细上下文(整段源码等),可多次调用累积


ask_expert(question) # 步骤2:提一个具体问题 → 触发对强模型的一次调用

emit_expert_context(expert.py:57)把上下文追加进一个全局字典 expert_context (expert.py:51);docstring 要求「宁多勿少但要信息密集、总量 500 词内」。ask_expert (expert.py:158)才是真正发问的那一下。

5.2 ask_expert 如何组装「一次性上下文」

关键设计:专家什么都不记得,每次都得把上下文喂全。 ask_expert 会现场从各处记忆仓库把材料 拉齐,拼成一个大 prompt:

# tools/expert.py:232 ask_expert —— 按固定顺序拼 query
if related_contents: query_parts += ["# Related Files", related_contents]
if formatted_research_notes: query_parts += ["# Research Notes", ...]
if key_snippets: query_parts += ["# Key Snippets", key_snippets]
if key_facts: query_parts += ["# Key Facts About This Project", key_facts]
if expert_context["text"]: query_parts += ["# Additional Context", ...]
query_parts += ["# Question", question]
query_parts += ["**DO NOT OVERTHINK**", "**DO NOT OVERCOMPLICATE**"] # expert.py:252

材料来源:相关文件全文由 read_related_filesread_files_with_limit 读入(上限 10000 行, expert.py:100),加上 key facts / snippets / research notes(都从 03 说的 SQLite 记忆里取)。注意结尾硬塞的两句 DO NOT OVERTHINK——docstring 里也承认「专家容易想太多」 (expert.py:173),这是给强模型的「刹车」。

5.3 用哪个模型:get_model 与 expert 专属配置

get_model(expert.py:31)决定专家是谁:优先读 expert_provider / expert_model,没配就退回 主 agent 的 provider / model,再交给 initialize_expert_llm(llm.py:784)初始化。这意味着你 可以给 expert 单独指定一个更强的模型,而日常推理仍用便宜的。模型只初始化一次并缓存在全局 _model

5.4 什么时候该问专家:prompt 里的注入点

「何时求助专家」不是硬编码的,而是写在给主 agent 的系统提示里。prompts/expert_prompts.py 针对研究 / 规划 / 实现 / 聊天四个阶段各有一段 EXPERT_PROMPT_SECTION_*(expert_prompts.py:10 起),被拼进对应阶段的提示。四段都反复强调同一条边界:

专家擅长逻辑、调试、规划,但只能看到你给它的上下文,够不到外部世界;想查最新信息应改用 web search 工具。(expert_prompts.py:17 等,四段末尾复述)

这就把两条通道的分工说清了:逻辑难题 → 问 expert;信息缺口 → 用 web search。


6. 人类在环(HIL):把决定权交回给人

要解决的小问题: 有些事 AI 不该自己拿主意——需求有歧义、要选技术方向、要确认危险操作。这时最好 的「工具」是直接问人。

ask_human(human.py:26)弹一个面板显示问题,用 prompt_toolkit 开一个支持多行输入的会话 (Ctrl+D 提交,human.py:47-55),把人的回答作为字符串返回给 agent。

一个容易忽略的细节:回答会入库。 人的每次输入都写进 human_input 表,并按来源打标签 (chathil):

# tools/human.py:65 ask_human —— 回答落库并打标签
if get_config_repository().get("chat_mode", False):
source = "chat"
elif get_config_repository().get("hil", False):
source = "hil"
human_input_repo.create(content=response, source=source)
human_input_repo.garbage_collect() # 只保留最近 100 条

为什么要存?因为几乎每个工具在记 trajectory 时都会带上「最近一条 human_input 的 id」 (比如 shell.py:74),用它把「机器做的动作」挂到「触发它的那次人类请求」上——人类输入是整条 执行轨迹的锚点(细节见 03)。garbage_collect() 保证这张表不会无限 膨胀,只留最近 100 条。

expert 和 human 的分工: 两条都是「求助」通道,但方向相反——expert 是向上求助(更强的机器 算力),human 是向外求助(人的判断/授权)。前者解决「我算不出来」,后者解决「我不该替你决定」。


7. 工具如何按阶段组合(tool_configs)

前面所有工具不是一股脑全给 agent 的。tool_configs.py 是「工具装配车间」,按阶段和开关现拼清单。

7.1 分层的工具组

底座是只读工具 get_read_only_tools(tool_configs.py:126),各阶段都在它之上叠加:

get_read_only_tools ──┬─→ get_research_tools() 研究阶段:只读 + 研究 + (可选)expert
(read_file + ├─→ get_planning_tools() 规划阶段:只读 + 规划 + expert
run_shell_command) ├─→ get_implementation_tools() 实现阶段:只读 + 改文件 + expert
└─→ get_web_research_tools() 网络研究:tavily + notes

一个有意思的细节:run_shell_command 被放进了「只读」组,代码里带着一句自嘲的注释——它其实 改文件,但只读任务也离不开它:

# tool_configs.py:153 get_read_only_tools
run_shell_command, # can modify files, but we still need it for read-only tasks.

get_all_tools(tool_configs.py:165)则把所有组拼在一起,供需要全量工具的场景使用。

7.2 改文件工具的动态切换:set_modification_tools

§3.2 vs §3.3 的「用 aider 还是用原生工具」这个二选一,就落在这里。set_modification_tools (tool_configs.py:37)按 use_aider 开关替换整个 MODIFICATION_TOOLS 列表:

# tool_configs.py:37 set_modification_tools
if use_aider:
MODIFICATION_TOOLS[:] = [run_programming_task] # 走 aider 子进程
else:
MODIFICATION_TOOLS[:] = [file_str_replace, put_complete_file_contents] # 走原生工具

这就是「一个 CLI flag(--use-aider)改变 agent 手里拿的是哪把改代码的工具」的实现机制。

7.3 自定义工具与 MCP

get_custom_tools(tool_configs.py:52)支持从用户指定的 Python 模块动态加载额外工具:该模块 要导出一个 tools 列表(langchain 的 tool 对象),加载后缓存进全局 CUSTOM_TOOLS,并要求每个工具 返回带 success/can_retry/return_code/output 的字典(can_retry=True 时可带上次输出重试)。

更外部的扩展是 MCP(Model Context Protocol):utils/mcp_client.py 里的 MultiServerMCPClient_Sync(mcp_client.py:8)把上游异步的 langchain-mcp-adapters 客户端包一层 同步外壳(单独线程跑 event loop),再把每个 MCP 服务器暴露的异步工具动态生成成同步的 StructuredTool(mcp_client.py:_wrap_async_tool),这样 MCP 工具就能和内置工具一样被 agent 调用。 get_tools_sync(mcp_client.py:131)返回这批包装好的工具。


8. 边界与局限

  • cowboy_mode 是全有或全无。 一旦选 c,本 session 之后所有命令都不再确认——没有「只对某类命令 放行」的中间档(shell.py:106)。
  • 专家是「无记忆」的。 每次 ask_expert 都得把上下文重新喂全;忘了 emit_related_files / emit_expert_context,专家就看不到那部分代码(expert.py:168-169 docstring 明说)。
  • 专家够不到外部世界、也没有最新信息。 逻辑题找它,查资料得走 web search(expert_prompts.py 四段末尾反复强调)。
  • aider 路径只能增/改文件,不能删。 删除得回落到 run_shell_command(programmer.py:48)。
  • web search 依赖 Tavily。 没有 TAVILY_API_KEY 就直接抛异常(web_search_tavily.py:45)。
  • 各工具的输出都被 5000 行截断。 超长输出会丢尾部(text/processing.py:7 truncate_output)—— 这是保护上下文窗口的必要妥协,但也意味着 agent 可能看不到被截掉的部分。

9. 代码地图(导航索引)

用符号名 grep 定位比行号更抗漂移。

主题文件路径符号名
跑 shell / cowboy_mode 审批ra_aid/tools/shell.pyrun_shell_command_detect_shell
aider 集成 / 拼子进程命令ra_aid/tools/programmer.pyrun_programming_taskparse_aider_flags
精确字符串替换(唯一性护栏)ra_aid/tools/file_str_replace.pyfile_str_replace
整文件写入ra_aid/tools/write_file.pyput_complete_file_contents
读文件(拒二进制 + 5000 行限制)ra_aid/tools/read_file.pyread_file_tool
列目录树(默认不递归)ra_aid/tools/list_directory.pylist_directory_tree
正则搜索 + 默认排除目录ra_aid/tools/ripgrep.pyripgrep_searchDEFAULT_EXCLUDE_DIRS
模糊找文件名ra_aid/tools/fuzzy_find.pyfuzzy_find_project_files
Tavily 网络搜索ra_aid/tools/web_search_tavily.pyweb_search_tavily
专家:攒上下文 / 提问 / 选模型ra_aid/tools/expert.pyemit_expert_contextask_expertget_modelread_related_files
专家初始化ra_aid/llm.pyinitialize_expert_llm
专家 prompt 注入点(四阶段)ra_aid/prompts/expert_prompts.pyEXPERT_PROMPT_SECTION_RESEARCH
人类在环(回答入库)ra_aid/tools/human.pyask_human
工具按阶段装配ra_aid/tool_configs.pyget_read_only_toolsget_all_toolsget_custom_toolsset_modification_tools
MCP 工具同步包装ra_aid/utils/mcp_client.pyMultiServerMCPClient_Syncget_tools_sync
输出行截断ra_aid/text/processing.pytruncate_output