Skip to content

Skill evolution is a no-op: optimized_module.skill_text is the unchanged baseline #141

Description

@sunghunkwag

The skill-evolution pipeline cannot, by construction, modify the skill text it claims to evolve. The "evolved" SKILL.md written to output/ is byte-identical to the input.

Root cause. In skill_module.py, the skill body is stored as a plain instance attribute and passed as a DSPy InputField value:

self.skill_text = skill_text                 # plain attribute
...
skill_instructions=self.skill_text,          # passed as an input VALUE

DSPy optimizers (GEPA, MIPROv2, BootstrapFewShot) only mutate a Predict submodule's signature.instructions and demos. A plain attribute is never registered as a parameter, so compile() never touches it. evolve_skill.py:183 then reads the untouched attribute back:

evolved_body = optimized_module.skill_text   # == baseline, always

Reproduction (no API key needed, dspy 3.x):

import dspy
class TaskWithSkill(dspy.Signature):
    skill_instructions: str = dspy.InputField()
    task_input: str = dspy.InputField()
    output: str = dspy.OutputField()
class SkillModule(dspy.Module):
    def __init__(self, skill_text):
        super().__init__(); self.skill_text = skill_text
        self.predictor = dspy.ChainOfThought(TaskWithSkill)
m = SkillModule("ORIGINAL BODY")
print([n for n,_ in m.named_parameters()])   # ['predictor.predict']
print("skill_text" in dict(m.named_parameters()))  # False
print(m.deepcopy().skill_text == m.skill_text)     # True  -> never changes

Consequence for the Phase-1 report. The reported 0.408 → 0.569 (+39.5%) cannot have come from a changed skill. With BootstrapFewShot (per the report) the only thing optimized is bootstrapped demos attached to the predictor — and reassemble_skill() discards them, writing only frontmatter + unchanged body. The headline gain is a transient demo-augmented surrogate that is never saved or deployed.

Proposed fix (verified to round-trip). Make the skill body the optimizable signature.instructions, and read it back from there:

class SkillModule(dspy.Module):
    def __init__(self, skill_body: str):
        super().__init__()
        sig = dspy.Signature("task_input -> output", instructions=skill_body)
        self.predictor = dspy.ChainOfThought(sig)

    def forward(self, task_input: str) -> dspy.Prediction:
        result = self.predictor(task_input=task_input)
        return dspy.Prediction(output=result.output)

    @property
    def skill_body(self) -> str:
        return self.predictor.predict.signature.instructions
# evolve_skill.py
evolved_body = optimized_module.skill_body   # was optimized_module.skill_text

Verified: after an instruction optimizer rewrites predictor.predict.signature.instructions, skill_body returns the new text — the optimized artifact is now the saved artifact. This fix is a prerequisite for the two companion issues (the bag-of-words fitness metric / dead LLMJudge, and the silently-swallowed dspy.GEPA(..., max_steps=...) call) to have any effect; I'll cross-link them here once filed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions