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 负责生成 prompt,TaoToken 负责把 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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_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.0"ignore这个 crate 是关键,它直接复用 .gitignore 的匹配语义,还能自动跳过.git目录和二进制文件,省得我们自己写一堆过滤逻辑。
2.4 拿 Key 与配置环境变量
去 TaoToken 控制台的 API Keys 页面创建一个 Key,然后写进环境变量,别硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的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: Option<PathBuf>, /// 单文件最大字节数,超过则跳过 #[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::Result<Vec<(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: BTreeMap<String, Vec<String>> = BTreeMap::new(); for p in paths { let parts: Vec<&str> = 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: Vec<String> = 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 refused
error 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 input
failed 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 只是起点,真正的功夫在于你对自己项目的理解——知道哪些文件对当前任务重要,比任何工具都关键。