Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

小羽 · Xiaoyu

ci PyPI

Weaving code, connecting dots, and showing you the best harness architecture.

一个自建的 harness coding agent。零第三方运行依赖(只有 openai SDK), Windows / macOS / Linux 全平台,pip install xiaoyu-agent 即用。

名字取自董永传说里的七仙女天羽——织女织布,小羽织代码。羽毛在传统语义里是飞升与轻盈, 对应这个 agent 想要的手感:行云流水、轻量、无负担。

顺带一个巧合:harness 本身就是织机的部件(提综装置,控制经线升降的那套框架)。 所以"织女 + harness"不是比附,是同一个词。

现在能做什么(v0.10)

  • 任意 OpenAI 兼容端点(LiteLLM / vLLM / 各家官方 API),流式输出
  • 六个基础工具exploreskill 另见下方):
    • read_file(支持 offset / limit 只读一段)
    • grep(正则搜索,自动跳过 .git / node_modules / __pycache__ 等噪声)
    • list_files(glob 列文件,输出统一正斜杠)
    • str_replace(精确替换,主要编辑手段)
    • write_file(整文件覆盖,只用于新建或全量重写)
    • bash(Windows 上自动换 PowerShell 执行,工具描述与 system prompt 按平台生成)
  • explore 子 agent:便宜模型 + 只读工具做检索,返回带 路径:行号 + 原文行的结论。 详见下方「explore 与实测数据」
  • 编辑护栏:改已有文件必须先完整 read_filebash cat 不算,只读一段也不算); 读完之后文件被外部改动会拒绝写入并要求重读;old_str 不唯一或匹配不上会带行号提示打回; 行中间开始 + 多行替换会被拦(必然破坏缩进)
  • 分层上下文回收:超阈值先 microcompact(把较早的大块 read_file/grep/bash 输出替换成占位符——不花模型调用、不磨损结论,够用就不做摘要); 不够再全量摘要——本地估算 token + 用真实 usage 校准,早期历史交给便宜模型总结。 原始任务永久保留,摘要不层层累加,压缩后反而更大则放弃; 断路器:连续两次压缩省不到 10% 就暂停自动压缩(手动 /compact 不受限)
  • 权限规则allow bash(git *) / deny bash(curl *) / allow write_file(src/*) 这类 规则免逐次确认或直接拦截;deny 在任何模式下都生效(包括 --yolo; 复合命令每一段都要被 allow 覆盖,含 $( ) / 反引号 / > 的命令不吃 allow 前缀规则; 规则放用户级 permissions.txt 或仓库 .xiaoyu/permissions.txt,REPL 里 /allow /deny 直接写入、/perm 查看;确认框答 a = 本会话该工具不再问
  • 危险命令硬拦截rm -rf /、fork bomb、mkfsdd 直写块设备、Windows format 等不可撤销操作在任何模式下都不执行,包括 --yolo——审批是"用户想不想",这层是"绝不"
  • 项目级指令文件:读仓库根目录的 AGENTS.md(或 XIAOYU.md / CLAUDE.md,首个命中) 进 system prompt——项目自带的规范(怎么跑测试、代码约定)跟着仓库走,不用每次口头交代
  • 插件工具:第三方包在 entry point 组 xiaoyu.tools 里声明工厂函数, pip install 后自动挂载(这是接内部工具的代码层通道);坏插件只警告不拦启动、 不许覆盖内置工具、未声明的能力按需要确认处理(fail-closed)
  • 会话落盘:交互与一次性执行的每条消息 append 到用户目录 sessions/*.jsonl (首行 meta,压缩/清屏记事件),供事后诊断,也是将来 /resume 的地基
  • SKILL.md 技能:扫描 ~/.agents/skills/(跨客户端规范库)与用户配置目录 skills/, 与 Anthropic / agentskills.io 同形态;渐进披露——索引进 system prompt, 正文由模型用 skill 工具按需加载,/skills 查看; 索引有预算(单条描述 ≤250 字符、总量 ≤上下文窗口 1%),技能装再多也不吃常驻上下文
  • 错误分类与自动恢复:限流/瞬时错误按分类指数退避重试(±25% jitter 错峰, 服务端给了 Retry-After 就听它的),重试只在这一层(SDK 层已关,不会 3×3 叠加); 上下文超限先强制压缩再重试,鉴权错误直接报清楚不空转; 中断(Ctrl-C)后全量扫描补齐悬空的 tool 结果、半截流式回答也入历史,随时能继续对话
  • 循环护栏:撞到单轮工具调用上限时让模型收尾交代(做了什么/剩什么/建议), 不静默截断;连续相同 (工具, 参数) 调用第 3 次附加提示、第 5 次拒绝执行—— 便宜模型容易原地打转,得在 harness 层刹住
  • 工具可用性探测:工具可挂 check_fn,探测不过就不进 schemas、拒绝执行 (/tools 里标记 [不可用]
  • 写文件和执行命令默认逐个人工确认str_replace 显示 -/+ 差异预览
  • 交互 REPL(/help /tools /skills /model /usage /context /compact /perm /allow /deny /clear
    • 一次性执行模式 + 启动横幅;xiaoyu config 配置向导(全平台固定路径,免找 .env
  • 按模型分开记账的 token 统计;eval 可横向扫 12 个候选模型并算成本
  • 跨平台:Windows(PowerShell 分派、输出统一 UTF-8)/ macOS / Linux, CI 三平台 × 两 Python 版本矩阵验证

还没做:接内部工具(飞书 / EDW / Amazon 运营;SKILL.md + 插件 entry point 两条载体已就绪)、 /resume 恢复会话(落盘已就绪)、TUI、真沙箱隔离。

测试

# 单元测试(255 个,全部不打网络;CI 在三平台 × py3.11/3.14 跑同一套)
.venv/bin/python -m unittest discover -s tests -t .

# eval:真实调模型跑端到端任务
.venv/bin/xiaoyu-eval --list
.venv/bin/xiaoyu-eval                            # 全部 case
.venv/bin/xiaoyu-eval --case targeted_edit -v    # 单个 case + 完整输出
.venv/bin/xiaoyu-eval --model bedrock-claude-opus-5 --repeat 3

eval 集在测什么

case 卡的是什么
fix_and_test 修 bug + 加注解 + 自己写测试并真的跑通
targeted_edit 130 行文件里定点改 —— 用 diff 行数上限抓"整文件重写"
readonly_answer 只读任务一个字都不许改 —— 抓"手痒乱动文件"
multi_file_rename 跨 3 文件重命名,改完测试还得过 —— 抓"改一半"

判据全部机械可判(文件内容、diff 规模、测试退出码、用了哪个工具),没有主观评分。 结果存到当前目录 xiaoyu-eval-results/*.json,含 token、耗时、工具调用序列,用来比较改 prompt / 换模型前后的差异。 失败的 case 会额外保存现场(transcript + 最终文件内容)——临时工作区跑完就删,不留现场就没法诊断。

写新 case 的铁律:断言必须双向自证。先喂"已知正确答案"确认全 PASS, 再喂"看似完成但实际错"确认能 FAIL。这条已经固化成 tests/test_eval_assertions.py, 不打网络就能跑,加 case 时顺手补上正反两个 fixture。

踩过的三个坑(都会让你误判成"agent 不行"):

  • 初始文件因为 textwrap.dedent 找不到公共前缀而带着缩进写进去,语法直接错
  • file_contains("2 ") 判断指数退避,占位函数里的 return value * 2 也命中,等于永远通过
  • file_contains("ZeroDivisionError") 判断"处理了除零"——模型抛 ValueError 是同样合理的设计, 指令里没规定异常类型,这个断言等于偷偷加了一条没提的要求。判行为,别判字面

判行为用 python_snippet_ok:探针脚本写到工作区之外的临时文件、以工作区为 cwd 和 PYTHONPATH 运行, 既避开 shell 引号地狱,也不会污染 nothing_written / unchanged_except 的快照。

安装

pip install xiaoyu-agent      # 或 pipx install xiaoyu-agent
xiaoyu --version              # 验证装上了(多 Python 并存时也能确认升级生效在哪个环境)

升级:

pip install --upgrade xiaoyu-agent    # pipx 装的用:pipx upgrade xiaoyu-agent

开发模式:

git clone https://github.com/pholex/xiaoyu.git && cd xiaoyu
python3 -m venv .venv
.venv/bin/pip install -e .

配置

首次使用直接跑配置向导(全平台,写到固定的用户级路径,从此不用找 .env 放哪):

xiaoyu config            # 交互向导:端点、模型、key
xiaoyu config --show     # 查看生效配置与各项来源(key 永不回显)
xiaoyu config --path     # 打印用户级配置文件路径
xiaoyu config --set XIAOYU_MODEL=deepseek-v4-pro   # 非交互写入,可重复

用户级配置文件的位置:macOS / Linux 在 ~/.config/xiaoyu/.env(跟随 $XDG_CONFIG_HOME), Windows 在 %APPDATA%\xiaoyu\.env

也可以手动在任意工作目录放 .env(零依赖自解析,仓库里的已被 .gitignore 排除):

XIAOYU_BASE_URL=https://<你的网关>/v1
XIAOYU_MODEL=bedrock-claude-sonnet-5
XIAOYU_API_KEY=<你的-key>

优先级:真实环境变量 > 当前目录 .env > 项目根 .env > 用户级 .env,所以临时覆盖很方便:

XIAOYU_MODEL=bedrock-claude-opus-5 xiaoyu

macOS 上 key 也可以不落盘,改用 Keychain(.env 里留空即可,会自动回退去读;Windows 上请用 .env 或环境变量):

security add-generic-password -a "$USER" -s "XIAOYU_API_KEY" -U -w
变量 默认值 说明
XIAOYU_BASE_URL —(必填) 任意 OpenAI 兼容 /v1 端点:LiteLLM、vLLM、各家官方 API…
XIAOYU_MODEL deepseek-v4-pro 见下方"选模型"
XIAOYU_API_KEY 端点的 API key
XIAOYU_ENV_FILE 指定 .env 路径,等价于 --env-file

xiaoyu                                  # 交互模式(xy 是等价缩写)
xy "把 utils.py 里的类型注解补全"          # 一次性执行
xiaoyu --model bedrock-claude-opus-5    # 指定模型

REPL 里:/help /tools /skills /model /usage /context /compact /clear /exit

选模型

默认 主模型 deepseek-v4-pro + 摘要 deepseek-v4-flash,国产便宜模型优先。

依据是 12 个候选模型 × 4 个 case 的实测(xiaoyu/evals/results/*sweep.json):

模型 相对输入单价 4 case 单个 case 成本
deepseek-v4-flash 4/4 $0.0026
qwen3.7-plus 4/4 $0.0052
deepseek-v4-pro 4/4 $0.0061
glm-5.2 4/4 $0.011
gpt-5.6-luna 4/4 $0.016
qwen3.7-max 12× 4/4 $0.030
kimi-k3 20× 4/4 $0.034
gpt-5.6-terra 19× 3/4 $0.037
bedrock-claude-sonnet-5 20× 4/4 $0.062
gpt-5.6-sol 37× 4/4 $0.099
bedrock-claude-opus-5 37× 4/4 $0.124
bedrock-claude-fable-5 68× 4/4 $0.147

同一个任务,最贵的比最便宜的高 57 倍,而通过率没差别 —— 所以默认取便宜的。

⚠️ 但这张表不能用来证明"便宜模型够用":12 个模型几乎全部满分,说明 当前 eval 集没有区分度,4 个 case 都是单文件小改或机械重命名,任何能正常调工具的模型都做得到。 用"全都满分"的 eval 选模型等于抛硬币。真要有依据,得补能让模型露馅的 case: 需要迭代调试的、大文件多处精确编辑的、指令自相矛盾需要顶回来的、长上下文触发压缩的、 以及"不该动的别动"。这件事按当前模型能力性价比不高,暂时搁置。

硬活手动升级,别指望默认模型包打天下:

xiaoyu --model bedrock-claude-opus-5     # 启动时指定
# 或 REPL 里随时切:/model bedrock-claude-opus-5

关于 token usage:12 个候选全都回传 usage(含 gpt-5.6-*),所以压缩的 token 校准 在所有模型上都有效。注意这跟"Mantle / Responses API 不回 usage"的经验相反 —— 经 /v1/chat/completions + stream_options.include_usage 这条路是回的。

explore 与实测数据

explore 把检索委托给便宜模型的只读子 agent(默认 deepseek-v4-flash), 它只有 read_file / grep / list_files——绝不给 bash,否则「只读」是空话 (有测试实测跑完这三个工具后整个工作区字节不变)。

在一个「4 层间接跳转 + 每层都有诱饵常量」的多跳追踪任务上量了五组:

配置 主模型 in tok 总成本 是否用了 explore
A 关闭 explore 24165 $0.01168
B 开启,弱引导 25398 (+5%) $0.01238 (+6%) 没用
C 开启,强制使用 11925 (-51%) $0.00957 (-18%) 用了
D 强引导,自主 29097 (+20%) $0.01640 (+40%) 用了,但又重读了 8 个文件
E 修好证据行 + offset 25804 (+7%) $0.01237 (+6%) 没用

五组数据给出的结论,每一条都反直觉:

  1. 用了确实有效(C):主模型上下文砍一半、总成本降 18%。主 agent 从 9 次工具调用降到 3 次。 主模型越贵收益越大——flash 是 1×、deepseek-v4-pro 是 3×,换成 opus-5(37×)差距会拉到十几倍。
  2. 靠 prompt 引导的采用率只有 1/3(B、D、E 三次里只有 D 主动用了)。措辞劝不动模型。
  3. 挂上不用也要付钱(B):工具 schema 每轮随请求发送,光是存在就 +5%。工具不能无限加。
  4. D 组暴露的是真 bug:模型给 read_file 传了 offset 参数(主流 harness 的标准签名), 我们没实现 → 8 次读有 4 次报废。只有走「explore 之后再重读」这条路径才会触发,前三组碰不到。
  5. 模型重读是合理的:D 组它自己说「链已经清晰了,但让我验证 FORWARD_TO 确实被使用而非 FALLBACK」。 当时 explore 只返回路径行号、没有原文,而任务里警告了有诱饵——不信是对的。 所以现在要求子 agent 必须给出原文证据行,并说明排除了哪些干扰项。

因为第 2 条,采用率改成 harness 层面强制而不是继续改措辞: 连续 3 次 read_file 追加提示,连续 5 次直接拦截并要求改用 explore用任何其它工具即重置计数——这样「读那几个马上要改的文件」不会被误伤。 没挂 explore 时该机制完全不触发(劝它用一个不存在的工具是荒谬的)。

方法论上最值钱的一条:「没被调用」不等于「没有用」。A、B 两组里 explore 一次没被调用, 当时差点直接删掉;是 C 组「强制用一次」才量出 -51%。功能没被采用功能没有价值 是两个独立问题,必须分开验证。

⚠️ 安全

bash 工具会在你机器上执行模型给出的任意命令。默认每条都要你确认,这是主要防线; deny 权限规则和危险命令硬拦截在 --yolo 下仍然生效,但覆盖面有限。 --yolo 会关掉逐条确认——只在一次性、可丢弃的目录里用。

已经踩过一次:eval 是无人值守 + --yolo 跑的,某个模型跑 pytest 失败后执行了 pip install pytest,装进了系统 Python 的 site-packages(那个目录 admin 组可写、免 sudo)。 现在 eval 会注入 PIP_REQUIRE_VIRTUALENV=true 挡住这条路,但要清楚: 这只堵了一个具体出口,不是沙箱read_file / str_replace / write_file 有工作区边界检查, bash 没有——它仍能写你有权限的任何地方。真隔离要靠容器或 sandbox-exec

结构

xiaoyu/                     仓库根
├── pyproject.toml          注册 xiaoyu / xy / xiaoyu-eval 三个命令;依赖精确锁版本
├── .env                    运行配置(含 key,已 gitignore)
├── .github/workflows/      CI(三平台矩阵 + 打包验证)与 release(tag → PyPI 自动发布)
├── .githooks/pre-push      本地兜底:push 前跑全部测试
├── experiments/            可复现实验脚本(README 里的数字出处)
├── tests/                  单元测试 255 个,全部不打网络
│   ├── test_tools.py             工具层、编辑护栏、连续读拦截、硬拦截、平台分派
│   ├── test_context.py           token 估算、压缩、断路器、消息序列合法性
│   ├── test_microcompact.py      microcompact 分层回收、压缩摘要 prompt 结构
│   ├── test_permissions.py       权限规则解析、判定管线、会话授权、Agent 集成
│   ├── test_plugins.py           entry_points 插件加载、fail-closed 默认、工具顺序稳定
│   ├── test_readonly_tools.py    grep / list_files、explore 只读边界
│   ├── test_agent_paths.py       主循环、中断恢复、配对补齐、项目指令、打转检测(假 client)
│   ├── test_errors.py            错误分类器、Retry-After/jitter、重试/压缩恢复路径
│   ├── test_skills.py            SKILL.md 解析、扫描、渐进披露、索引预算、check_fn
│   ├── test_user_config.py       xiaoyu config、用户级 .env、平台路径
│   ├── test_session_log.py       会话落盘
│   ├── test_banner.py            启动横幅
│   ├── test_models.py            候选模型、成本计算、横向对比排序
│   └── test_eval_assertions.py   eval 断言的双向自证
└── xiaoyu/                 包
    ├── config.py           运行配置 + .env 解析链 + key 读取(永不回显)
    ├── tools.py            工具注册表、基础工具、护栏、硬拦截、插件加载、平台分派
    ├── permissions.py      权限规则:allow/deny、bash 前缀、路径 glob、会话授权
    ├── agent.py            主循环:流式、tool_calls 累加、审批、分层回收、恢复、循环护栏
    ├── errors.py           API 错误分类器(限流/瞬时/超限/鉴权)+ Retry-After 解析
    ├── explore.py          explore 子 agent(便宜模型 + 只读工具)
    ├── skills.py           SKILL.md 技能:扫描、frontmatter、渐进披露
    ├── compaction.py       上下文压缩:切点、摘要、回退保护、断路器
    ├── session_log.py      会话落盘(JSONL)
    ├── tokens.py           本地 token 估算 + 用真实 usage 校准
    ├── cli.py              REPL、斜杠命令、config 子命令、确认交互
    ├── banner.py           启动横幅(窄终端降级)
    ├── ui.py               ANSI 输出、Windows VT/UTF-8 适配
    └── evals/              eval 集(放包内,避免占用 `evals` 这个通用顶层名)
        ├── harness.py      Case/Context + 断言原语
        ├── cases.py        具体任务
        ├── models.py       12 个候选模型 + 成本计算
        ├── prices.json     单价(随包分发;改价需手工更新)
        ├── runner.py       执行器(支持 --sweep 横向扫模型)
        └── results/        历史跑分归档(仅在仓库;新结果写到当前目录 xiaoyu-eval-results/)

外层 xiaoyu/ 是仓库、内层是包,这是 Python 的标准形态(同 requests/requests),不是冗余。

路线(按价值排,不按容易排)

  1. str_replace 编辑工具 — v0.2(严格匹配 + 唯一性校验 + 先读再改 + 失败带提示回错)
  2. eval 集 — v0.2 建成,但当前没有区分度(12 个模型几乎全满分), 要有依据得补「需要迭代调试 / 大文件多处精确编辑 / 指令自相矛盾 / 长上下文 / 该克制不动」这类硬 case。 按当前模型能力性价比不高,已搁置
  3. 上下文压缩 — v0.3(本地估算 + usage 校准 + 安全切点 + 摘要不累加 + 变大则回退)
  4. 模型路由 — v0.3/v0.4(摘要与 explore 走便宜模型;主模型默认换成国产便宜模型)
  5. explore 子 agent — v0.4(便宜模型 + 只读工具 + harness 层面强制采用)
  6. 接内部工具 — 飞书、EDW、Amazon 运营那套。这才是自建 harness 相对 Codex 的真实价值, 下一个大动作。载体已就绪:v0.8 起支持 SKILL.md(与 ~/.agents/skills/ 规范库直接互通); v0.10 起有代码层通道——entry point 组 xiaoyu.tools,内部工具包 pip install 即挂载。
  7. 真沙箱隔离(容器 / sandbox-exec)——v0.7 先落了危险命令硬拦截兜底, 但 --yolo 下仍无真正边界,沙箱才是完整答案。
  8. CI / 发布流水线 — v0.9.1 全部就位:GitHub Actions 三平台 × 两 Python 版本测试 + 打包验证;推 vX.Y.Z tag 即经 PyPI Trusted Publishing(OIDC,无长期 token)自动发布, 含 tag 与 __version__ 一致性检查;本地 pre-push hook 兜底。 发版流程 = 改 __init__.py 版本号 → commit → 推 tag,其余全自动。
  9. /resume 恢复会话(会话落盘已就绪)、prices.json 自动同步、TUI 精致化。

About

小羽 — a harness coding agent

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages