
1. 为什么我要自己写一个 Code2Prompt 风格的 CLI你有没有过这种体验想让 Claude Code 或 Codex 帮忙改一段登录逻辑结果它先find . -type f列一遍文件再grep -rn login搜一遍关键词然后对候选文件逐个 Read最后才拼出一个它自认为理解的项目视图。整个过程又慢又费 token而且它读到的往往是路径拼接、文件头注释、空行、配置文件这些对当前任务几乎没用的内容。Code2Prompt 这个思路就是来解决这个问题的一条命令把整个项目变成一张结构化的 LLM prompt。它用 Rust 写GitHub 上已经有 7.5k star。核心能力是把源码目录树、文件内容、Git 元数据、token 计数打包成一段 LLM 能直接理解的项目快照。但直接用现成工具有时候不够灵活——你可能想自定义忽略规则、想控制输出格式、想把它嵌进自己的脚本里。所以这篇我带你从零用 Rust 写一个 Code2Prompt 风格的 CLI把目录遍历、忽略规则、prompt 拼装这几件事讲透。适合有 Rust 基础、想把上下文工程自动化的人。我试过让 Agent 自己翻项目一个 Next.js 仓库能扫出几万个文件真正相关的可能就几十个。与其让 Agent 用 find grep 拼一个残缺视图不如我们主动生成一份结构完整的 prompt 喂给它。2. 前置准备Rust 环境与 TaoToken 接入写这个 CLI 之前先把两件事准备好Rust 工具链以及一个能调用的 LLM 接口。CLI 负责生成 promptTaoToken 负责把 prompt 投喂给模型验证效果。2.1 Rust 工具链如果你还没装 Rust用 rustup 一行搞定curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env rustc --version cargo --version确认版本在 1.75 以上因为后面会用到一些较新的标准库特性。2.2 为什么用 TaoToken 做验证端生成的 prompt 总得有个地方投喂。TaoToken 提供统一的 API 入口兼容 OpenAI 风格的接口你拿一个 Key 就能调多种模型。对于这个 CLI 的验证场景来说好处是prompt 生成完直接curl一下就能看到模型对项目结构的理解不用来回切平台。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 这个不加 UTM。你需要先去控制台拿一个 API Key后面验证请求会用到。2.3 项目初始化新建一个 Rust 二进制项目cargo new code2prompt-rs cd code2prompt-rs编辑Cargo.toml把依赖配好。这里用ignore处理 .gitignore 规则ripgrep 同款库性能好walkdir做目录遍历兜底clap解析命令行参数anyhow做错误处理[package] name code2prompt-rs version 0.1.0 edition 2021 [dependencies] clap { version 4.5, features [derive] } ignore 0.4 walkdir 2.5 anyhow 1.0ignore这个 crate 是关键它直接复用 .gitignore 的匹配语义还能自动跳过.git目录和二进制文件省得我们自己写一堆过滤逻辑。2.4 拿 Key 与配置环境变量去 TaoToken 控制台的 API Keys 页面创建一个 Key然后写进环境变量别硬编码进代码export TAOTOKEN_API_KEYsk-你的key如果你用的是 Claude Code 这类工具配置里需要写全三件套——Base URL、Key、Model ID缺一不可{ base_url: https://taotoken.net/api, api_key: sk-你的key, model: claude-sonnet-4-20250514 }这个 JSON 片段后面在验证环节会直接用到。注意 Base URL 用https://taotoken.net/api不要带 UTM 参数那是给网页链接用的。3. 可复制配置目录遍历与忽略规则实现这一节是核心。我们把 CLI 拆成三块参数解析、目录遍历、prompt 拼装。每一块都给完整可复制的代码。3.1 命令行参数定义用 clap 的 derive 模式定义参数。我们要支持指定项目路径、输出文件、最大文件大小限制、是否包含 Git 信息。use clap::Parser; use std::path::PathBuf; #[derive(Parser, Debug)] #[command(name c2p, version, about 把项目打包成 LLM prompt)] struct Args { /// 项目根目录 #[arg(default_value .)] path: PathBuf, /// 输出文件不填则打印到 stdout #[arg(short, long)] output: OptionPathBuf, /// 单文件最大字节数超过则跳过 #[arg(long, default_value_t 100_000)] max_size: u64, /// 是否包含 Git 元数据 #[arg(long, default_value_t false)] git: bool, }max_size默认 100KB防止某个巨大的 lock 文件或压缩包把 prompt 撑爆。3.2 用 ignore crate 做目录遍历这是整个工具的灵魂。ignore::WalkBuilder会自动读取 .gitignore、.ignore还能配置是否跳过隐藏文件use ignore::WalkBuilder; use std::fs; fn collect_files(root: std::path::Path, max_size: u64) - anyhow::ResultVec(String, String) { let mut files Vec::new(); let walker WalkBuilder::new(root) .hidden(true) // 跳过隐藏文件 .git_ignore(true) // 遵循 .gitignore .git_global(true) // 遵循全局 gitignore .git_exclude(true) // 遵循 .git/info/exclude .build(); for result in walker { let entry result?; if !entry.file_type().map_or(false, |ft| ft.is_file()) { continue; } let metadata entry.metadata()?; if metadata.len() max_size { continue; } let path entry.path(); let rel path.strip_prefix(root)?.to_string_lossy().to_string(); // 只处理文本文件二进制直接跳过 let content match fs::read_to_string(path) { Ok(c) c, Err(_) continue, }; files.push((rel, content)); } files.sort_by(|a, b| a.0.cmp(b.0)); Ok(files) }几个细节值得说hidden(true)会跳过.git、.env这类隐藏项read_to_string失败说明是二进制文件直接continue跳过最后按路径排序保证每次生成的 prompt 顺序一致方便 diff。3.3 生成目录树LLM 需要空间信息才能理解模块关系。我们用一个简单的缩进树来表示use std::collections::BTreeMap; fn build_tree(paths: [String]) - String { let mut tree: BTreeMapString, VecString BTreeMap::new(); for p in paths { let parts: Vecstr p.split(/).collect(); if parts.len() 1 { let dir parts[..parts.len() - 1].join(/); tree.entry(dir).or_default().push(parts[parts.len() - 1].to_string()); } else { tree.entry(..to_string()).or_default().push(p.clone()); } } let mut out String::new(); for (dir, files) in tree { out.push_str(format!({}/\n, dir)); for f in files { out.push_str(format!( {}\n, f)); } } out }3.4 拼装最终 prompt把目录树、文件内容、可选的 Git 信息拼成一段结构化文本fn build_prompt(root: std::path::Path, files: [(String, String)], git: bool) - String { let paths: VecString files.iter().map(|(p, _)| p.clone()).collect(); let tree build_tree(paths); let mut prompt String::new(); prompt.push_str(# 项目快照\n\n); prompt.push_str(format!(根目录: {}\n\n, root.display())); prompt.push_str(## 目录结构\n\n\n); prompt.push_str(tree); prompt.push_str(\n\n); if git { if let Ok(branch) std::process::Command::new(git) .args([rev-parse, --abbrev-ref, HEAD]) .current_dir(root) .output() { prompt.push_str(format!(当前分支: {}\n\n, String::from_utf8_lossy(branch.stdout).trim())); } } prompt.push_str(## 文件内容\n\n); for (path, content) in files { prompt.push_str(format!(### {}\n\n\n{}\n\n\n, path, content)); } prompt }3.5 main 函数串起来fn main() - anyhow::Result() { let args Args::parse(); let root args.path.canonicalize()?; let files collect_files(root, args.max_size)?; let prompt build_prompt(root, files, args.git); let token_estimate prompt.len() / 4; eprintln!(文件数: {}, 预估 token: {}, files.len(), token_estimate); match args.output { Some(p) std::fs::write(p, prompt)?, None println!({}, prompt), } Ok(()) }prompt.len() / 4是个粗略的 token 估算英文代码大致 4 字符 1 token够用了。4. 验证请求一条命令生成 prompt 并投喂给 AI代码写完编译并跑起来cargo build --release ./target/release/c2p . --output prompt.md --git你会看到 stderr 打印出文件数和预估 token同时prompt.md里是完整的项目快照。打开看一眼目录树在最上面每个文件内容用代码块包着结构清晰。4.1 用 curl 投喂给模型拿到 prompt 后直接调 TaoToken 的接口验证模型能不能理解项目结构PROMPT$(cat prompt.md) curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d $(jq -n --arg p $PROMPT { model: claude-sonnet-4-20250514, messages: [ {role: user, content: (这是一个 Rust CLI 项目请分析它的模块划分和主要职责\n\n $p)} ] })这里用jq -n --arg是为了安全地把大段 prompt 塞进 JSON避免转义问题。4.2 期望的成功结果模型返回的内容应该能准确说出项目有几个模块、collect_files负责遍历、build_prompt负责拼装、依赖了ignore和clap。如果它能复述出目录树里的文件名说明空间信息传递到位了。对比一下如果你只把main.rs单独贴给模型它看不到Cargo.toml就不知道依赖了什么看不到目录结构就不知道模块怎么组织。这就是结构化 prompt 的价值。4.3 用 MCP 模式让 Agent 自主调用如果你想让 Claude Code 或 Cursor 直接调用这个工具可以把它包成 MCP 服务器。核心思路是暴露一个get_project_context工具Agent 传路径进来你返回 prompt。这样 Agent 就不用自己 find grep 了直接拿到结构化上下文。配置 MCP 时同样要写全三件套{ mcpServers: { code2prompt: { command: ./target/release/c2p, args: [--output, -], env: { TAOTOKEN_API_KEY: sk-你的key } } } }Base URL、Key、Model ID 这三样在 Agent 侧配置里缺一不可否则调用会失败。5. 本篇常见错误排查写完跑起来大概率会踩几个坑。这里列几个真实报错和对应解法。5.1 401 Unauthorized{error:{message:Invalid API key,type:invalid_request_error}}原因通常是环境变量没生效或者 Key 复制时带了空格。检查echo $TAOTOKEN_API_KEY | head -c 10确认前缀是sk-且没有换行。如果用的是 Claude Code 配置检查 JSON 里api_key字段有没有写错。5.2 local proxy failed / connection refusederror sending request: error trying to connect: tcp connect error这个一般是 Base URL 写错了。确认是https://taotoken.net/api不要漏掉/api也不要带 UTM 参数。如果你在配置文件里写了https://taotoken.net/api/v1而代码里又拼了一次/v1就会变成/api/v1/v1同样报错。5.3 reading choices: unexpected end of JSON inputfailed to parse response: reading choices: unexpected end of JSON input这个报错说明返回体不是合法 JSON常见原因是 prompt 太大导致请求被截断或者jq拼 JSON 时转义失败。解法先用小项目测试确认链路通了再上大仓库用jq -n --arg而不是字符串拼接。5.4 OAuth / 认证方式不匹配OAuth authentication is not supported for this endpoint如果你在 Claude Code 里配了 OAuth 登录又同时想用 API Key会冲突。统一用 API Key 方式把 OAuth 相关配置清掉。5.5 生成的 prompt 里混进了 node_modules如果发现目录树里全是依赖包说明 .gitignore 没生效。检查项目根目录有没有.gitignore或者用--max-size限制单文件大小。ignorecrate 只在有 .gitignore 时才按规则过滤没有的话它只跳过隐藏文件。5.6 中文文件名乱码to_string_lossy()在极端情况下会替换非法字符。如果你的项目有中文路径建议在Cargo.toml里确认 edition 是 2021标准库对 UTF-8 处理已经够用。真遇到乱码检查终端 locale 设置。6. 把上下文工程变成你的日常习惯写到这里这个 CLI 已经能跑通完整链路了遍历项目、应用忽略规则、生成结构化 prompt、投喂给模型验证。你可以把它加到 shell alias 里alias c2p~/code2prompt-rs/target/release/c2p以后在任意项目目录下c2p . --output /tmp/prompt.md就能拿到一份项目快照。几个实用技巧给不同任务准备不同的忽略规则比如做代码审查时排除测试文件做架构分析时只保留入口文件把生成的 prompt 存成带时间戳的文件方便对比不同版本的项目结构token 估算超过模型窗口时用--max-size调小阈值或者先按目录分批生成。如果你想让 Agent 长期自主调用这个能力建议走 Coding Plan 那条路把 MCP 服务器配好让 Agent 自己决定什么时候拉取项目上下文。需要 Key 的话去 API Keys 页面创建接入细节看接入文档。验证模型对 prompt 的理解效果可以直接在模型对话里贴一段试试。上下文工程的核心不是把整个项目扔进去而是选对文件、用对格式、带上空间信息。这个 CLI 只是起点真正的功夫在于你对自己项目的理解——知道哪些文件对当前任务重要比任何工具都关键。