最新资讯
dev.to 如何在专用 GPU 服务器上用 OpenAI 兼容 API 部署 LLM dev.to 审计 153 份 Claude Code 与 Codex 会话记录:User-Agent 与 URL 中的邮箱泄露 dev.to 你的 Agent Harness 应具备的 17 项能力 dev.to 离线 AI 记忆中 0.8 余弦阈值为何失效 dev.to Kimi K3.1 Pro 开放权重:自托管 2.8T 模型前的开发者清单 dev.to 如何通过工具调用引导模型以提升 Harness 质量 dev.to AI Agent 审计轨迹不等于对话记录 dev.to 精简 MCP server 工具数量:更少工具、更小影响面,双重收益 dev.to 为什么 git checkout 无法撤销 AI 编码 agent 的改动? wechat AI短剧门槛降降降!实测即梦短剧工作室:剧本分镜预演一条龙,伙伴计划长期护航 dev.to 让模型改写文档前,先量化事实保真度 dev.to 回放 153 个 Claude Code 与 Codex 会话,审计 Agent 的生产影响范围
多元动态
登录 注册
智能体 dev.to · @Casey Chen

额外参数即发布:审查扩展签名的 Agent PR

原文 我的收藏

文章提出审查 AI 代理生成的代码时,应将新增的默认参数视为发布行为,并给出信任、回滚和测试的具体建议,包括 AST 差异分析。

AI推荐理由 开发者社区文章,针对 AI 生成代码的审查方法,提供具体策略,对开发实践有参考价值。

70

正文

An optional parameter is a release. Coding agents add them so a new branch compiles, then attach a default that every old caller inherits. Review that default as shipped behavior, not as leftover style. This article is a review method for agent-generated pull requests that grow public signatures, exports, and CLI flags. It states what to trust, what to revert, and what to test. It also includes a proposed AST diff you can run on two worktrees. The failure mode Agents optimize for the call site they can see. They do not inventory every downstream importer, wrapper script, or dashboard that shells out to your CLI. A defaulted parameter changes behavior for callers that never pass it. A newly public name is a support promise. A flag that defaults to on rewrites production argv you did not grep. This is not hidden I/O, swallowed exceptions, or tests that encode the bug. The diff lives in the signature. The regression shows up two services away. Define the contract before arguing about taste For a Python package, treat the following as public unless the repo already documents a narrower rule: Module-level names that do not start with _ Names listed in __all__ Symbols re-exported from __init__.py argparse / click / typer options and their defaults Dataclass fields and typed-dict keys that serialize to JSON or ORM rows Private helpers (_parse) can move. Public names cannot. If the agent promotes _parse to parse so a unit test can import it, that is a product decision. Revert the promotion or accept the support cost. Artifact: two-tree signature diff The script below is a proposed, unexecuted example. It walks two source trees (base vs PR) and prints added public callables, added parameters, and changed defaults. Run it on a checkout. Do not treat its output as a security audit. #!/usr/bin/env python3 """signature_diff.py — compare public function signatures between two trees.""" from __future__ import annotations import ast import sys from dataclasses import dataclass from pathlib import Path @dataclass(frozen=True) class Param: name: str kind: str default: str | None @dataclass(frozen=True) class FuncSig: qualname: str params: tuple[Param, ...] returns: str | None def _rel(path: Path, root: Path) -> str: return path.relative_to(root).as_posix() def _is_public(name: str) -> bool: return not name.startswith("_") def _dump_default(node: ast.expr | None) -> str | None: if node is None: return None return ast.dump(node, include_attributes=False) def _params(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[Param, ...]: args = fn.args out: list[Param] = [] def take(items: list[ast.arg], kind: str, defaults: list[ast.expr | None]) -> None: padded: list[ast.expr | None] = [None] * (len(items) - len(defaults)) + list(defaults) for arg, default in zip(items, padded): out.append(Param(arg.arg, kind, _dump_default(default))) take(args.posonlyargs, "posonly", []) take(args.args, "pos_or_kw", list(args.defaults)) if args.vararg: out.append(Param(args.vararg.arg, "vararg", None)) take(args.kwonlyargs, "kwonly", list(args.kw_defaults)) if args.kwarg: out.append(Param(args.kwarg.arg, "kwarg", None)) return tuple(out) def _skip_path(path: Path) -> bool: parts = set(path.parts) if parts & {"venv", ".venv", "site-packages"}: return True if "tests" in parts or path.name.startswith("test_"): return True return False def walk(root: Path) -> dict[str, FuncSig]: found: dict[str, FuncSig] = {} for path in root.rglob("*.py"): if _skip_path(path): continue try: tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) except SyntaxError: continue mod = _rel(path, root).replace("/", ".").removesuffix(".py") for node in tree.body: if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): if not _is_public(node.name): continue q = f"{mod}:{node.name}" ret = ast.dump(node.returns, include_attributes=False) if node.returns else None found[q] = FuncSig(q, _params(node), ret) elif isinstance(node, ast.ClassDef) and _is_public(node.name): for item in node.body: if not isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): continue if not _is_public(item.name): continue q = f"{mod}:{node.name}.{item.name}" ret = ast.dump(item.returns, include_attributes=False) if item.returns else None found[q] = FuncSig(q, _params(item), ret) return found def diff(base: dict[str, FuncSig], pr: dict[str, FuncSig]) -> None: print("## added public callables") for k in sorted(set(pr) - set(base)): names = ", ".join(p.name for p in pr[k].params) print(f"+ {k}({names})") print("## removed public callables") for k in sorted(set(base) - set(pr)): print(f"- {k}") print("## signature changes") for k in sorted(set(base) & set(pr)): if base[k].params == pr[k].params and base[k].returns == pr[k].returns: continue b = {p.name: p for p in base[k].params} p = {p.name: p for p in pr[k].params} added = [n for n in p if n not in b] dropped = [n for n in b if n not in p] changed = [n for n in p if n in b and p[n].default != b[n].default] print(f"* {k}") if added: print(f" added params: {added}") for n in added: print(f" default={p[n].default}") if dropped: print(f" dropped params: {dropped}") if changed: print(f" changed defaults: {changed}") for n in changed: print(f" {b[n].default} -> {p[n].default}") if base[k].returns != pr[k].returns: print(f" return: {base[k].returns} -> {pr[k].returns}") def main(argv: list[str]) -> int: if len(argv) != 3: print("usage: signature_diff.py BASE_TREE PR_TREE", file=sys.stderr) return 2 diff(walk(Path(argv[1])), walk(Path(argv[2]))) return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv)) Enter fullscreen mode Exit fullscreen mode Checkout both sides, then run: git fetch origin pull/123/head:pr-123 git worktree add /tmp/pr-base origin/main git worktree add /tmp/pr-head pr-123 python3 signature_diff.py /tmp/pr-base /tmp/pr-head Enter fullscreen mode Exit fullscreen mode Pair the AST walk with a grep for CLI and env-shaped contracts the parser will miss: git diff origin/main...HEAD -- '*.py' \ | rg -n 'add_argument\(|click\.(option|command)|typer\.Option|os\.environ|getenv\(' Enter fullscreen mode Exit fullscreen mode The script will not see C extensions, *args forwarding that later binds names, or module-level __getattr__. Use it as a net, not as a proof. Decision table: trust, revert, test Diff class Trust? Revert? Test New _-prefixed helper, no re-export Yes, if old public paths still have tests No Existing callers only New public function used only by tests No Yes, or keep it private Import linter: tests must not introduce new public names New param whose default is None and skips a prior required path No Yes, unless the skip is an intentional product change Omitted arg equals old behavior; explicit None is documented New param whose default changes a timeout, limit, batch size, or retry No Revert the default; require explicit opt-in Bound test with the old numeric value as the implicit default Default value changed (limit=100 → limit=1000) No Restore the old default; add a named constant if needed Snapshot callers that omit the arg New required param No, unless this is an intentional major Restore a default or split a new function Compile downstream examples New CLI flag defaulting to on No Default off, or drop the flag Run the documented command line with no new flags Return type widened (list[User] → `list[User] \ None`) No Keep the old type; raise or return empty Re-export from __init__.py Only if it was already public Drop the re-export Freeze from package import name "Trust" means: leave it in the merge. It does not mean the agent understood production. Review sequence that stays on the signature Generate the signature diff. If it is empty and the CLI grep is empty, stop this checklist and review other classes of bug. For each added parameter, write the implied call in the review comment: foo(a, b) before, foo(a, b, c=DEFAULT) after. If DEFAULT is not observationally equal to the old body, the PR is a behavior change. Reject DEFAULT=None as a stand-in for "do the new thing." None is a value. Callers pass it by accident. Reject promotions made for test convenience. If a test needs a hook, keep the hook private or add a supported testing entrypoint. Freeze one documented import list and one documented command line in CI. Agent PRs that change either must update the freeze file in the same commit. Proposed freeze test (unexecuted example): # tests/test_public_names_freeze.py import json from pathlib import Path import myservice as pkg FREEZE = Path(__file__).parent / "public_names.json" def test_public_names_match_freeze(): names = sorted(n for n in dir(pkg) if not n.startswith("_")) expected = json.loads(FREEZE.read_text()) assert names == expected, ( "Public names changed. If intentional, update public_names.json " "and the changelog in the same PR." ) Enter fullscreen mode Exit fullscreen mode That test is boring. Boring is the point. Agents will edit the freeze file if the instructions allow it. Treat a freeze-file edit as a product change, same as a schema migration. Where a scratch model and server help Reproducing two worktrees and running signature_diff.py needs a clean machine, not a chat transcript. If you do not want extra worktrees on a laptop, MonkeyCode's free model access and free server option can hold the checkout and run the commands above. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Use the model only to draft the missing tests for each added default. Do not let it "fix" the freeze file. The reviewer still classifies each row in the decision table. Skip any hosted model if the tree is proprietary and policy forbids upload. The method does not depend on a vendor. What to test when you keep a new default If the team accepts a new optional parameter, the minimum suite is three cases, not one: Omission. Call the old arity. Assert the old observable result, including timing bounds if the default is a timeout or batch size. Explicit old value. Pass the previous implicit value. Assert equality with omission. If they diverge, the default is not the old behavior. Explicit new value. Pass the agent's new path. Assert the new result, and assert it cannot be reached by omission. For CLI flags, replace step 1 with the argv vector copied from a runbook. Do not invent a "typical" command line. Copy one from production logs or from the README operators already use. Label any load or latency claim as unmeasured unless a bench already lives in the repo. A default limit change is a performance change even when CPU tests stay green. Worked review comment (proposed text, not a recorded incident): NACK default. `fetch_page(limit=1000)` changes every caller that omitted `limit`. Old body used 100. Restore default=100. Keep `limit=` as opt-in. Add tests: omit == 100, limit=100 == omit, limit=1000 is the new path only. Enter fullscreen mode Exit fullscreen mode Limitations The AST walker ignores dynamically constructed signatures, functools.wraps mismatches, and protocols implemented in another language. __all__ lies. Prefer dir() on the installed package over reading __all__ by eye. Dataclass field defaults and Pydantic Field(default=...) are public contract. The sample script does not parse them. Extend it or review those files by hand. Internal-only apps with a single binary and no importers can skip the freeze file. They should not skip CLI default review. Generated protobuf or OpenAPI stubs are the schema, not the agent's Python wrapping. Review the schema file, then the wrapper. This method will not catch authorization gaps, oracle-contaminated tests, or hidden network calls. Those need other checklists. Skip this workflow when the PR is a typo, a comment, or a pinned dependency with no signature change. Apply it when the agent "just added a parameter so the new branch compiles." Teams with an existing API-freeze bot can keep the bot and still use the decision table. The table is the review standard. The script is only a finder. Closing An omitted argument is a caller. New defaults ship to that caller without a changelog line. Diff the signatures. Revert convenience exports. Keep new behavior behind explicit arguments. Freeze one import list and one command line. That is the whole method.

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

← 返回精选动态 原文

更多内容