最新资讯
www.infoq.com GitLab 19.5 将 Agentic 开发纳入 DevOps 平台的治理体系 dev.to 在信任 AI 生成的测试用例前,如何逐条复核 unite.ai Franklin Templeton 构建 AI 研究工作流以揭示风险 techxplore.com AI 助力寻找更优的钛合金 3D 打印工艺参数 engadget.com Perplexity 推出“teammates”日常工作任务 AI agent computerworld.com 别提数据中心:OpenAI 改称“compute campuses” theguardian.com 俄美试图阻止就自主武器制定具有约束力的规则 bloomberg.com Nvidia 加强东南亚芯片追踪以打击走私 thestack.technology 模型路由需求升温,Agent Router 推出开源替代方案 www.youtube.com Google DeepMind 的 Philipp Schmid:AI 同事需要具备的七个要素 unite.ai Cashfree 为 AI Agent 的托管结账加入 WebMCP 工具 dev.to 我们在 DolphinBench 上测试记忆检索:纯关键词搜索暴露了自身回归
多元动态
登录 注册
行业动态 dev.to · @Masih Maafi

Aider 内部工作原理剖析

原文 我的收藏

文章通过跟踪一条消息在 Aider Python 源码中的流转,说明其与 Codex 的设计差异:Codex 中模型拥有工具并自行探索仓库,而 Aider 中模型没有工具,由 harness 决定模型看到的内容,模型以纯文本作答,harness 再从文本中提取编辑并应用。文中代码引自 Aider 0.86.3.dev(commit 5dc9490),作者声明未实际运行,仅基于源码阅读。

AI推荐理由 基于 Aider 0.86.3.dev 源码逐段解读其架构:模型无工具、由 harness 决定上下文并解析纯文本编辑,与 Codex 的工具调用设计形成对比,对理解编码 Agent 实现有参考价值;但作者明确说明未实际运行,仅为源码阅读。

72

正文

Codex and Aider look alike from the outside: you type, the model edits your code. Inside they are opposite designs. In Codex the model has tools and explores the repository itself. In Aider the model has no tools at all. The harness decides what the model sees, the model answers in plain text, and the harness finds the edits in that text and applies them. This post follows one message through Aider's Python source, the same way my Codex and Elpis post does for Codex. Code is quoted verbatim from Aider 0.86.3.dev, commit 5dc9490, with paths relative to the repository root. Nothing here was run; it is read from the source. 1. The shape: one object per mode main() builds a single Coder and loops on coder.run(). Each edit format is its own Coder subclass with its own prompts and parser, so a mode change such as /ask or /architect is not a flag. It raises SwitchCoder, and main builds a new Coder from the old one, copying the files, history and cost across (in Coder.create). # aider/main.py while True: try: coder.ok_to_warm_cache = bool(args.cache_keepalive_pings) coder.run() analytics.event("exit", reason="Completed main CLI coder.run") return except SwitchCoder as switch: # … kwargs = dict(io=io, from_coder=coder) kwargs.update(switch.kwargs) # … coder = Coder.create(**kwargs) Enter fullscreen mode Exit fullscreen mode If the edit format changed, the old history is summarized first, because the code's own comment says the old format "will confused the new LLM. It may try and imitate it". # aider/coders/base_coder.py # If the edit format changes, we can't leave old ASSISTANT # messages in the chat history. The old edit format will # confused the new LLM. It may try and imitate it, disobeying # the system prompt. done_messages = from_coder.done_messages if edit_format != from_coder.edit_format and done_messages and summarize_from_coder: try: done_messages = from_coder.summarizer.summarize_all(done_messages) Enter fullscreen mode Exit fullscreen mode 2. What the model sees The request is assembled by ChatChunks in a fixed order: system prompt, examples, read-only files, the repo map, past history, the editable files, the current turn, and last a reminder of the edit-format rules. # aider/coders/chat_chunks.py def all_messages(self): return ( self.system + self.examples + self.readonly_files + self.repo + self.done + self.chat_files + self.cur + self.reminder ) Enter fullscreen mode Exit fullscreen mode done is the past history and cur is the current turn. Stable parts come first and volatile ones last, so the prompt cache keeps a long prefix (my inference from the caching code below). Notice that the editable files sit after the history, so the model reads the newest version of a file right next to your request. Context blocks are faked as conversation: a message such as "here are the files" followed by an invented "Ok." from the assistant. A history summary comes back the same way, as a user message beginning "I spoke to you previously about..." (see summarize_all in aider/history.py and summary_prefix in aider/prompts.py). The model sees these as things that were said, not as notes from the system. The whole conversation goes to litellm, one API over many providers, at temperature 0 by default (in Model.send_completion). Retryable errors back off from 0.125 seconds, doubling until the delay passes 60. # aider/models.py RETRY_TIMEOUT = 60 # … retry_delay = 0.125 # … if should_retry: retry_delay *= 2 if retry_delay > RETRY_TIMEOUT: should_retry = False Enter fullscreen mode Exit fullscreen mode 3. No tools, on purpose The base class sets functions = None, and the reflection limit sits right next to it. # aider/coders/base_coder.py functions = None # … num_reflections = 0 max_reflections = 3 Enter fullscreen mode Exit fullscreen mode Three coders once used JSON function calls to edit, but they are out of the registry, and two raise "Deprecated" when built (in editblock_func_coder.py and wholefile_func_coder.py). The only reason the repository gives is a changelog note from v0.7.0 in HISTORY.md: "Initial experiments show that using functions makes 3.5 less competent at coding". So the model writes edits as fenced text, and Aider finds them with regular expressions. The nearest thing to a shell tool is a fenced bash block in the reply, which runs only if you say yes (in run_shell_commands). Its output goes into the next turn, not back to the model at once. That changes what "agentic" means. Codex loops until the model stops asking for tools. Aider's only loop is a reflection: after a turn, if Aider itself found a problem, it sends the problem back as the next user message, at most three times (the max_reflections = 3 above). Four things count as a problem: an edit that would not parse or match, a repo file the model named that is not in the chat, lint errors, and test errors. They all share the same budget of three. That second one is how the model "opens a file". It cannot call a read tool, so it names the file in its reply. Aider notices, asks you whether to add it, and replays the turn. The check runs before any edit is applied, so an otherwise valid edit in that same reply is thrown away. # aider/coders/base_coder.py if not interrupted: add_rel_files_message = self.check_for_file_mentions(content) if add_rel_files_message: if self.reflected_message: self.reflected_message += "\n\n" + add_rel_files_message else: self.reflected_message = add_rel_files_message return Enter fullscreen mode Exit fullscreen mode The early return comes before apply_updates() is ever called. A person is the gate where Codex would have a tool call. 4. From a reply to a change on disk The default format is SEARCH/REPLACE: the model quotes the lines to change, then the lines to put there. The parser is loose on purpose, accepting five to nine marker characters and hunting up to three lines above a block for its filename, because one model kept misplacing them (in find_original_update_blocks and find_filename). # aider/coders/editblock_coder.py HEAD = r"^?\s*$" DIVIDER = r"^={5,9}\s*$" UPDATED = r"^>{5,9} REPLACE\s*$" # … # if next line after HEAD exists and is DIVIDER, it's a new file if i + 1 2 and not part_lines[0].strip(): skip_blank_line_part_lines = part_lines[1:] res = perfect_or_whitespace(whole_lines, skip_blank_line_part_lines, replace_lines) # … res = try_dotdotdots(whole, part, replace) # … return # Try fuzzy matching res = replace_closest_edit_distance(whole_lines, part, part_lines, replace_lines) Enter fullscreen mode Exit fullscreen mode perfect_or_whitespace covers the first two steps. Keep an eye on the bare return near the end; section 10 comes back to it. When a block fails, the error the model receives is written to be fixable: each failed block echoed back, the closest real lines in the file, a reminder that SEARCH must match exactly including whitespace, and "The other N blocks were applied successfully. Don't re-send them." (in EditBlockCoder.apply_edits). # aider/coders/editblock_coder.py res += ( "The SEARCH section must exactly match an existing block of lines including all white" " space, comments, indentation, docstrings, etc\n" ) if passed: pblocks = "block" if len(passed) == 1 else "blocks" res += f""" # The other {len(passed)} SEARCH/REPLACE {pblocks} were applied successfully. Don't re-send them. Just reply with fixed versions of the {blocks} above that failed to match. Enter fullscreen mode Exit fullscreen mode Aider also has unified-diff, whole-file and OpenAI-style patch formats. The whole-file format has no matching step at all: the file is simply overwritten. 5. The repo map: context without a model choosing it Aider never lets the model search the repository, so it hands over a map. For every file it uses tree-sitter to pull out definitions and references, and caches them on disk keyed by modification time (in RepoMap.get_tags and get_tags_raw, in aider/repomap.py). Then it builds a graph. Files are nodes, and an identifier that one file defines and another references becomes an edge from the referencing file to the defining one. The weight is a multiplier times the square root of the number of references, so a name repeated a hundred times does not drown out the rest. The multiplier is 10 if you mentioned the name, 10 if it looks distinctive (long snake, kebab or camel case), 0.1 if it starts with an underscore, 0.1 if more than five files define it, and a further 50 if the edge comes from a file already in the chat (in RepoMap.get_ranked_tags). # aider/repomap.py if ident in mentioned_idents: mul *= 10 if (is_snake or is_kebab or is_camel) and len(ident) >= 8: mul *= 10 if ident.startswith("_"): mul *= 0.1 if len(defines[ident]) > 5: mul *= 0.1 # … if referencer in chat_rel_fnames: use_mul *= 50 # … num_refs = math.sqrt(num_refs) G.add_edge(referencer, definer, weight=use_mul * num_refs, ident=ident) Enter fullscreen mode Exit fullscreen mode "Long" means at least eight characters. Personalized PageRank then ranks the files, seeded by the chat files and anything you mentioned (the nx.pagerank call in the same function). No language model is involved. The score is split over each file's definitions, definitions in chat files are dropped because their full text is already sent, and Aider binary-searches for the largest map that fits the token budget, stopping once it is within 15% of it (in RepoMap.get_ranked_tags_map_uncached). # aider/repomap.py pct_err = abs(num_tokens - max_map_tokens) / max_map_tokens ok_err = 0.15 if (num_tokens best_tree_tokens) or pct_err 1: # raise SearchTextNotUnique() if num == 0: return new_text = original_text.replace(search_text, replace_text) return new_text Enter fullscreen mode Exit fullscreen mode /drop a.py also drops data.py. Dropping matches by substring of the path, where /add matches exactly or by glob (in cmd_drop). # aider/commands.py # For editable files, use glob if word contains glob chars, otherwise use substring if any(c in expanded_word for c in "*?[]"): matched_files = self.glob_filtered_to_repo(expanded_word) else: # Use substring matching like we do for read-only files matched_files = [ self.coder.get_rel_fname(f) for f in self.coder.abs_fnames if expanded_word in f ] Enter fullscreen mode Exit fullscreen mode What this does not cover I did not trace ContextCoder, watch mode's internals beyond the outline, voice input, the web scraper, or analytics. Aider can be run with a different edit format per model, and the defaults live in a settings file I read only through git. Originally published at https://masihmoafi.com/blog/aider-under-the-hood.

来源报道 · 1 篇 · 以下链接均前往原始媒体

Masih Maafi Aider 内部工作原理剖析 09-26 · 原文发布
← 返回精选动态 原文

更多内容