SkillOpt — ReflACT¶
Skill-document self-evolution. Train a single markdown skill doc as the external state of a frozen agent, with optimizer discipline. Runs through
evolve()with a customStrategy+aggregator_factory. Example:examples/skillopt/skillopt_skill_training.py.
| Paper | SkillOpt: Executive Strategy for Self-Evolving Agent Skills — Yang et al., 2025 (arXiv:2605.23904) |
| Repo | microsoft/SkillOpt (PyPI skillopt) |
| Dataset | SearchQA (single-turn text QA), EM / F1 |
| Layer | L2 skill (blast_radius=0.2) |
The algorithm (ReflACT)¶
Four load-bearing invariants, reproduced from the repo (engine/trainer.py,
optimizer/skill.py, evaluation/gate.py, optimizer/scheduler.py):
- Bounded string edits on one markdown doc — ops
{append, insert_after, replace, delete}(apply_patch). The doc is the whole trainable state, injected into the frozen agent by prompt concatenation (zero deployment calls). - Strict held-out accept gate — a candidate is accepted only if it strictly
improves the held-out validation hard-EM over the current skill (default
gate_metric=hard). Greedy hill-climbing — the same shape asevolve(). - Textual learning-rate budget — an integer cap on edits per step
(
optimizer/scheduler.py); AgentDescent'strust_region_opsanalogue. - Rejected-edit buffer — rejected edits are remembered in-epoch and fed back to the optimizer so it stops re-proposing them.
How it plugs into evolve()¶
strategy=SkillDocStrategy(ctx)— the analyst's edit patch → aDiffon the one-slot skill document (and it recordsdiff_id → editsfor the buffer).propose— the analyst (returns a budget-capped patch, sees the buffer).aggregator_factory→StrictGateAggregator— the strict-EM gate; it commits the best strictly-improving candidate as the dev head, buffers rejected edits, and advances the LR schedule each round.
A shared SkillOptContext (buffer, LR budget, edit registry, stats) is closed
over by both the propose step and the aggregator.
The epoch-level slow-update and meta-skill stabilisers are optional in the repo and omitted from this minimal-but-faithful slice.
Plug-ins implemented¶
In examples/skillopt/skillopt_skill_training.py:
| Plug-in | evolve() slot |
What it does |
|---|---|---|
SkillDocStrategy |
strategy= |
turns the analyst's bounded edit patch (append/insert_after/replace/delete) into a Diff on the one-slot skill document |
StrictGateAggregator |
aggregator_factory= |
strict held-out-EM accept gate + the rejected-edit buffer (remembered in-epoch) |
LRScheduler |
(edit budget) | the integer "learning-rate" cap on edits per step (constant/linear/cosine) |
make_propose(...) |
propose= |
the analyst — one failed rollout → a budget-capped edit patch |
Measured — SearchQA with DeepSeek¶
--steps 5 --provider openai --model deepseek-v4-flash:
| full split | --hard subset |
|
|---|---|---|
| items | 120 train / 80 val / 80 test | 29 / 20 / 20 (69 of 280 kept) |
| val hard-EM, before → after | 0.900 → 0.900 | 0.250 → 0.500 |
| test hard-EM | 0.900 | 0.450 |
| edits accepted / rejected | 0 / 1 | 3 / 3 |
On the full split the seed skill already answers 9 of 10, so a skill document has nothing to add and the strict gate accepts no edit.
--hard keeps the 69 questions of 280 that the seed skill gets wrong, and on
those the skill document doubles hard-EM. The gate stays strict there too:
3 of 6 proposed edits are still rejected.
The two columns are different benchmarks
0.250 is not "worse than 0.900" — it is the score on a subset selected for being unsolved. Report which one you used.
Making it measurable — --hard¶
SearchQA is saturated for a strong model, so the run above proves the gate works
and nothing else. --hard keeps the dataset and drops the questions carrying no
signal: one pass of the seed skill over a wider pool, keeping only what it gets
wrong.
python -m examples.skillopt.skillopt_skill_training --hard \
--provider openai --model deepseek-v4-flash --steps 5 --yes
That makes the benchmark harder, so its numbers are not comparable with numbers
from the full split — say which you used. The underlying helper,
select_hard, works on any item list and any scorer.
Run it¶
python -m examples.skillopt.skillopt_skill_training --dry-run
python -m examples.skillopt.skillopt_skill_training --model claude-haiku-4-5 --lr 4
Offline tests: tests/test_skillopt_example.py.