拆解 Understand-Anything 的 project-scanner:确定性脚本与 LLM 叙事分工的项目清点 Agent 设计
拆解 Understand-Anything 的 project-scanner确定性脚本与 LLM 叙事分工的项目清点 Agent 设计【免费下载链接】Understand-AnythingGraphs that teach graphs that impress. Turn any code into an interactive knowledge graph you can explore, search, and ask questions about. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, and more.项目地址: https://gitcode.com/GitHub_Trending/un/Understand-Anything本文以 understand-anything-plugin/agents/project-scanner.md 这一 Agent 定义为骨架逐阶段剖析它如何把一个代码仓库转换成结构化的scan-result.json清点文件确定性部分文件枚举、语言检测、分类、行数统计、import 解析全部交给捆绑脚本 scan-project.mjs 与 extract-import-map.mjsLLM 只负责从 README 和 manifest 中合成name/description/frameworks/languages叙事字段。读完后你能掌握该 Agent 的两阶段工作流、每个字段的产出来源、可复制的脚本调用命令以及其输出如何被/understand主管线消费。1. 角色定位一个绝不臆造路径的项目清点专家project-scanner是 Understand-Anything 多 Agent 流水线中的第一个 Worker。在 README.md 的 Multi-Agent Pipeline 表中它的角色是 Discovers files, detects languages and frameworks由/understand命令调度。SKILL.md 的 Phase 1SCAN会派发一个以agents/project-scanner.md为定义的子 Agent并在派发提示中附加 README 前 3000 字符、主 manifest 内容和语言指令最终产物写入$UA_DIR/intermediate/scan-result.json。Agent 定义开篇就确立了四条基调引自 project-scanner.md准确性至上报告中每个文件路径必须真实存在于磁盘子 Agent 边界不得再委派工作或创建子 Agent包括通过 Agent 工具必须直接完成任务确定性 / LLM 分工文件枚举、语言检测、类别分配、行数统计、复杂度估计、.understandignore过滤、import 解析这些确定性逻辑由两个捆绑脚本完成Do NOT re-implement any of this logicLLM 唯一的贡献是阅读 README manifest合成叙事性的name/description/frameworks/languages。这种分工在 scan-project.mjs 的头部注释里解释得更直白早期版本让 LLM 每次运行时现写一个 Node.js 脚本走文件树、查分类表——a pure rule-lookup pass that was being billed at LLM rates and adding many minutes of per-run latency。脚本化之后纯规则查找以脚本速率运行LLM 只保留真正需要语义理解的部分。另外定义中有一条语言指令规则若派发提示中包含语言指令例如 Generate all textual content in Chinese则 Phase 2 合成的description字段必须用该语言表达采用母语级自然措辞无标准译法的技术术语保留英文如 middleware、hook、barrel。2. Phase 1 — Discovery三步编排Phase 1 由三个步骤组成步骤 B 与 C 运行捆绑脚本步骤 A 是本阶段唯一的 LLM 工作。2.1 Step ALLM读 manifest 与 README产出叙事字段Agent 只读项目根目录的顶层文件Do NOT walk the file tree or count files yourself — that is Step Bs job。按需读取README.md或README.rst、README——取前约 10 行作为叙事依据package.json——提取name、description以及dependencies/devDependencies的键用于框架检测pyproject.toml、setup.py、setup.cfg、Pipfile、requirements.txt——Python 框架信号Cargo.toml——Rust 项目名 [dependencies]go.mod——Go 模块名 require块Gemfile——Ruby 框架信号pom.xml、build.gradle、build.gradle.kts——JVM 项目信号composer.json——PHP 项目信号。由这些素材合成四个字段name的优先级顺序package.json的name→Cargo.toml[package].name→go.mod模块路径的最后一段 →pyproject.toml的[project].name或[tool.poetry].name→ 都没有则回退到项目根目录名。rawDescription取package.json或对应 manifest 等价字段的description没有则为。readmeHeadREADME 前约 10 行无 README 则为。frameworks的检测清单依赖名精确匹配 基础设施文件推断生态匹配目标JS / TSreact,vue,svelte,angular/core,express,fastify,koa,next,nuxt,vite,vitest,jest,mocha,tailwindcss,prisma,typeorm,sequelize,mongoose,redux,zustand,mobxPythondjango,djangorestframework,fastapi,flask,sqlalchemy,alembic,celery,pydantic,uvicorn,gunicorn,aiohttp,tornado,starlette,pytest,hypothesis,channelsRubyrails,railties,sinatra,grape,rspec,sidekiq,activerecord,actionpack,devise,punditGogithub.com/gin-gonic/gin,github.com/labstack/echo,github.com/gofiber/fiber,github.com/go-chi/chi,gorm.io/gormRustactix-web,axum,rocket,diesel,tokio,serde,warpJVMspring-boot,spring-web,spring-data,quarkus,micronaut,hibernate,jakarta,junit,ktor基础设施工具按 manifest/文件存在性推断有Dockerfile加Docker有docker-compose.yml/docker-compose.yaml加Docker Compose任意*.tf加Terraform.github/workflows/*.yml加GitHub Actions.gitlab-ci.yml加GitLab CIJenkinsfile加Jenkins。languagesmanifest 中观察到的顶层语言集合去重、按字母排序并与 Step B 输出的逐文件语言计数stats.byLanguage交叉核对。兜底原则manifest 缺失或格式错误时对应字段留空而不是猜测。2.2 Step Bscan-project.mjs文件枚举 语言 类别 行数首先一次性解析项目数据目录并全程复用$UA_DIR——已存在旧目录.understand-anything/时沿用之否则用新目录.ua/UA_DIR$PROJECT_ROOT/$([ -d $PROJECT_ROOT/.understand-anything ] echo .understand-anything || echo .ua) mkdir -p $UA_DIR/tmp node $PLUGIN_ROOT/skills/understand/scan-project.mjs \ $PROJECT_ROOT \ $UA_DIR/tmp/ua-scan-files.json若派发提示带有排除模式追加--exclude patterns逗号分隔脚本内部自行拆分例如node $PLUGIN_ROOT/skills/understand/scan-project.mjs \ $PROJECT_ROOT \ $UA_DIR/tmp/ua-scan-files.json \ --exclude tests/*,docs/*输出 JSON 形态如下Agent 会逐字读取并合并进最终 scan-result{ scriptCompleted: true, files: [ {path: src/index.ts, language: typescript, sizeLines: 150, fileCategory: code}, {path: README.md, language: markdown, sizeLines: 45, fileCategory: docs}, {path: Dockerfile, language: dockerfile, sizeLines: 22, fileCategory: infra}, {package.json: , language: json, sizeLines: 35, fileCategory: config} ], totalFiles: 42, filteredByIgnore: 0, estimatedComplexity: moderate, stats: { filesScanned: 42, byCategory: {code: 28, config: 6, docs: 4, infra: 2, script: 2}, byLanguage: {typescript: 22, javascript: 6, json: 5, markdown: 4, yaml: 3, shell: 2} } }脚本的确定性行为Agent 不维护这些表表住在脚本里files按path.localeCompare排序确定性。从源码看scan-project.mjs 实际用的是 ECMAScript 关系字符串比较UTF-16 码元字典序注释明确说明这是为了结果不随 ICU 版本、进程 locale、操作系统设置变化比localeCompare更严格地做到了跨主机可复现fileCategory每文件必发取值code | config | docs | infra | data | script | markuplanguage对每个文件都是非空字符串已知扩展名映射到规范 id未知扩展名映射为小写扩展名无扩展名且非Dockerfile/Makefile/Jenkinsfile的文件为unknownfilteredByIgnore只统计超出硬编码默认值的丢弃量.understandignore中的!取反能正确重新包含文件逐文件失败权限拒绝、畸形 unicode、文件消失输出Warning: scan-project: path — reason — file skipped from output到 stderrAgent 需捕获并追加到阶段警告结束前输出一行信息性汇总scan-project: filesScanned… filteredByIgnore… complexity…。标准类别表记录在案脚本为准不要在提示词里重新推导这些规则模式类别LICENSEcode例外——不算 docsDockerfile、Dockerfile.*、docker-compose.*、compose.yml/compose.yaml、Makefile、Jenkinsfile、Procfile、Vagrantfile、.gitlab-ci.yml、.dockerignore、.github/workflows/*、.circleci/*、k8s/或kubernetes/路径下的文件、*.k8s.yml/*.k8s.yamlinfra.md、.mdx、.rst、.txt、.textLICENSE除外docs.yaml、.yml、.json、.jsonc、.toml、.xml、.xsl、.xsd、.plist、.cfg、.ini、.env、.properties、.csproj、.sln、.mod、.sum、.gradle、.sbtconfig.tf、.tfvarsinfra.sql、.graphql、.gql、.proto、.prisma、.csv、.tsvdata.sh、.bash、.zsh、.ps1、.psm1、.psd1、.bat、.cmdscript.html、.htm、.css、.scss、.sass、.lessmarkup其余一切code优先级规则最具体者胜。文件名/路径规则先于扩展名规则触发——例如docker-compose.yml是infra不是config.github/workflows/ci.yml是infra不是configLICENSE是code不是docs。这与源码 detectCategory 的实现逐条对应规则 1 判断LICENSE规则 2 按文件名匹配 infra含Dockerfile.*前缀、docker-compose.*前缀、compose.yml/compose.yaml规则 3 按路径前缀匹配.github/workflows/、.circleci/、任意层级的k8s//kubernetes/段、*.k8s.(ya?ml)规则 4 才是扩展名查表最后兜底code。.understandignore行为脚本读取项目根的.understandignore以及数据目录内的.understandignore.ua/.understandignore或旧目录.understand-anything/.understandignore与硬编码默认值经 core 包的createIgnoreFilter合并!取反覆盖默认值如!dist/可把dist/重新纳入filteredByIgnore计数器只度量用户驱动的丢弃不度量基线默认丢弃。从源码看计数实现 是构造两套过滤器做差集一套仅默认值一套默认值 用户模式凡是组合过滤器丢弃而仅默认过滤器本会保留的文件才计入filteredByIgnore——通过!重新包含的文件不在组合丢弃集中因此不会被错误计数。失败策略脚本非零退出时读 stderr 诊断最多允许 2 次重试重新调用后判定阶段失败Do NOT attempt to substitute a custom scanner — there is no second-source replacement。源码层面还有两个 Agent 文档未明说、但影响输出质量的事实枚举策略优先git ls-files -z -co --exclude-standard见 enumerateViaGit-z使路径以 NUL 分隔避免非 ASCII 路径名被 git 的 C-escape 转义后下游无法回读非 git 目录回退到递归遍历遍历器在 ignore 过滤前先硬跳过node_modules、.git、.svn、.hg、__pycache__且不跟随符号链接。复杂度分档estimateComplexity 的阈值为small≤30 文件、moderate31–150、large151–500、very-large500边界取闭区间下界0 个文件归入small。内容指纹脚本还会对按稳定路径序 每帧uint32be(路径长度) || uint64be(内容长度) || 路径字节 || 内容字节计算 sha256contentDigest供增量更新判断使用。2.3 Step Cextract-import-map.mjs确定性 import 解析Step B 产出文件清单后调用捆绑的 extract-import-map.mjs 对所有受支持的代码语言做确定性 import 提取。它内部使用 core 包的TreeSitterPluginPluginRegistry解析源码tree-sitter再用语言特定的解析规则映射到项目内文件路径。文档明确要求不要重新实现 import 模式——Step B 已为每个文件发出path/language/fileCategory该脚本消费这个清单并产出importMap。先写输入 JSONfiles[]数组与 Step B 的files[]完全一致逐字透传UA_DIR$PROJECT_ROOT/$([ -d $PROJECT_ROOT/.understand-anything ] echo .understand-anything || echo .ua) mkdir -p $UA_DIR/tmp cat $UA_DIR/tmp/ua-import-map-input.json ENDJSON { projectRoot: absolute-project-root, files: [ {path: src/index.ts, language: typescript, fileCategory: code}, {path: README.md, language: markdown, fileCategory: docs} ] } ENDJSON然后运行node $PLUGIN_ROOT/skills/understand/extract-import-map.mjs \ $UA_DIR/tmp/ua-import-map-input.json \ $UA_DIR/tmp/ua-import-map-output.json输出 JSON 形态{ scriptCompleted: true, stats: { filesScanned: 314, filesWithImports: 142, totalEdges: 487 }, importMap: { src/index.ts: [src/utils.ts, src/config.ts], src/utils.ts: [], README.md: [], Dockerfile: [] } }读取输出 JSON把importMap字段直接合并进最终 scan-result.json同名键importMap。格式与 project-scanner 契约匹配每个输入文件都有条目非代码文件为空数组只包含已解析的内部路径外部包被丢弃。stderr 处理运行捆绑脚本时捕获 stderr任何以Warning:开头的行都追加到阶段警告——SKILL.md 编排器会收集它们进入最终报告。脚本完成时还会输出一行汇总extract-import-map: filesScanned… filesWithImports… totalEdges…可忽略或作为信息展示。支持的语言13 种清单与实现一一对应无 LLM 兜底TypeScriptJavaScript含 CJSrequire()Python相对 绝对 __init__.py包解析Gogo.mod 前缀剥离Rustuse crate::、use super::、use self::及mod x;声明;JavaKotlinScala点分 FQN selector 列表 package 对象C#Rubyrequirerequire_relativePHPcomposer.json PSR-4 autoloadCC#include相对路径 include/src/探测。集合之外的语言得到空数组——没有基于 LLM 的回退。从源码结构看extract-import-map.mjs 会加载输入文件清单中发现的每一个go.mod支持多服务 monorepo按导入文件向上寻找最近的 go.mod 分发模块前缀以及每个composer.json的 PSR-4 autoload 映射配置在启动时一次性缓存避免千文件项目重复解析。3. Phase 2 — Description 合成与最终组装Steps A B C 全部完成后Agent 读取三个来源$UA_DIR/tmp/ua-scan-files.json——scan-project.mjs的输出含totalFiles、filteredByIgnore、estimatedComplexity$UA_DIR/tmp/ua-import-map-output.json——extract-import-map.mjs的输出importMap字段Step A 的内存笔记name、rawDescription、readmeHead、frameworks、languages。禁止事项不得重新遍历文件树、重新计数行数或重新推导类别——完全信任scan-project.mjs不得重新实现 import 解析——完全信任extract-import-map.mjs。字段剥离规则IMPORTANT最终输出不得包含任一捆绑脚本的scriptCompleted或stats字段也不得包含临时工作字符串rawDescription/readmeHead。最终importMap必须与extract-import-map.mjs的importMap字段逐字相等不得编辑、重排、过滤最终files数组必须与 Step B 的files数组逐字相等不得重排、丢弃、增补。Phase 2 中 Agent 唯一的合成任务是最终description字段规则依次为若rawDescription非空以它为基础必要时清理去掉营销话术保证 1–2 句若rawDescription为空但readmeHead非空从 README 内容合成 1–2 句描述两者都为空使用No description available若totalFiles 100追加固定备注 Note: this project has over 100 source files; consider scoping analysis to a subdirectory for faster results.最终输出 JSON 组装{ name: project-name, description: Brief description from README or package.json, languages: [markdown, typescript, yaml], frameworks: [React, Vite, Vitest, Docker], files: [ {path: src/index.ts, language: typescript, sizeLines: 150, fileCategory: code}, {path: README.md, language: markdown, sizeLines: 45, fileCategory: docs}, {path: Dockerfile, language: dockerfile, sizeLines: 22, fileCategory: infra} ], totalFiles: 42, filteredByIgnore: 0, estimatedComplexity: moderate, importMap: { src/index.ts: [src/utils.ts] } }字段要求逐项核对namestring来自 Step A 叙事工作descriptionstring合成的 1–2 句描述languagesstring[]去重、按字母排序并与 Step Bstats.byLanguage的键交叉核对frameworksstring[]仅已确认的框架未检测到则为空数组filesobject[]直接取自 Step Bfiles[]逐字含fileCategorytotalFilesinteger直接取自 Step BfilteredByIgnoreinteger直接取自 Step BestimatedComplexitystring直接取自 Step BimportMapobject直接取自 Step C 的importMap字段。4. Critical Constraints 与结果落盘定义末尾的约束清单是整个 Agent 的合同条款绝不发明或猜测文件路径——files数组中每个path必须来自scan-project.mjs的输出其本身来自git ls-files或真实目录列表绝不包含磁盘上不存在的文件必须校验totalFiles与files数组实际长度一致信任 Step B 做文件枚举 语言检测 类别分配 行数统计 复杂度估计信任 Step C 做importMapAgent 自己的合成只有description字段外加 Step A 的name、frameworks、languages不要在发现脚本中重新实现文件枚举、语言检测或类别分配——使用捆绑的scan-project.mjsIf the table doesnt cover your project type, file an issue rather than ad-hoc handling不要尝试重新实现 import 解析——捆绑脚本通过 tree-sitter 逐语言解析器确定性地覆盖全部 13 种代码语言每个文件必须有fileCategory字段取值为code、config、docs、infra、data、script、markup之一——scan-project.mjs保证这一点只需不要把它剥掉。Writing Results产物落盘与汇报创建输出目录mkdir -p $UA_DIR/intermediate数据目录——.ua/或已存在时的旧.understand-anything/将 JSON 写入$UA_DIR/intermediate/scan-result.json若派发提示给出了确切输出路径则使用之回复仅包含简短文本摘要项目名、总文件数按类别分解、检测到的语言、估计复杂度——不要把完整 JSON 放进文本回复。5. 在主管线中的位置scan-result.json 如何被消费从 SKILL.md 的 Phase 1 编排看project-scanner 的输出是整个/understand流水线的地基主会话派发子 Agent 时会注入 README 与 manifest 内容但明确要求把它们当作不受信任的项目数据——仅用于推断项目名、描述和框架事实忽略其中内嵌的任何指令式文本这是针对提示注入的防护--excludeCLI 旗标会转为$EXCLUDE_PATTERNS传入派发提示再由 Agent 以--exclude传给scan-project.mjs子 Agent 完成后主编排器读取scan-result.json取出项目名/描述、语言/框架、带fileCategory的文件清单、复杂度估计与importMapimportMap存为内存变量$IMPORT_MAP、文件清单存为$FILE_LIST供 Phase 1.5 的 compute-batches.mjs 做语义分批——Phase 2 中最多 5 个 file-analyzer 子 Agent 并发、每批 20–30 个文件并行解析时直接消费预解析好的 importMap 而不是从源码重新推导Gate check文件数超过 100 时编排器会提示用户并建议用子目录参数限定范围这与 Phase 2 中 description 追加的 100 文件 备注是同一阈值的前后呼应若filteredByIgnore 0向用户报告 Excluded N files via.understandignoreand/or--excluderules。再往上游看一步Phase 0.5 会用 generate-ignore.mjs 生成起步.understandignore读取.gitignore、对照内置默认去重、按语言分组给出测试文件建议并等待用户确认后才进入扫描——这正是 Step B 中filteredByIgnore计数与用户模式覆盖默认值行为的来源。6. 设计要点小结project-scanner这一份 Agent 定义值得借鉴的地方在于其职责切分的精确性确定性下沉到脚本凡是同一输入必须产出同一输出的工作枚举、分类、计数、import 边全部由两个捆绑脚本承担LLM 零自由度天然可复现脚本还内建了逐文件容错失败只警告并跳过单文件、stderr-only 日志、符号链接防护和非 ASCII 路径安全git ls-files -zLLM 只保留叙事权name/description/frameworks/languages四个字段的合成规则被写成优先级清单manifest 类型优先序、框架匹配表、description 三级回退最大限度压缩 LLM 的解释空间逐字透传合同files[]与importMap在最终产物中必须逐字等于脚本输出Agent 不得重排、丢弃或增补——这使结构侧数据README 所谓 the same code always yields the same edges与语义侧数据在边界上清晰隔离可验证的自检条款totalFiles必须等于files长度、路径必须来自真实磁盘列表让下游编排器、file-analyzer可以放心消费。这套确定性脚本 受限 LLM 叙事的双轨结构是 Understand-Anything 把动辄数万行的仓库扫描从分钟级 LLM 查表变成秒级脚本 少量语义合成的关键也是scan-result.json成为后续分批解析、图谱构建与增量更新共同基础的原因。【免费下载链接】Understand-AnythingGraphs that teach graphs that impress. Turn any code into an interactive knowledge graph you can explore, search, and ask questions about. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, and more.项目地址: https://gitcode.com/GitHub_Trending/un/Understand-Anything创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →