ai-following-rules
npx skills add https://github.com/lebsral/dspy-programming-not-prompting-lms-skills --skill ai-following-rules
Agent 安装分布
Skill 文档
Make Your AI Follow the Rules
Guide the user through defining and enforcing rules their AI must follow. The key insight: don’t ask the AI to follow rules â program constraints that enforce them automatically.
The two types of rules
DSPy gives you two constraint primitives:
dspy.Assert |
dspy.Suggest |
|
|---|---|---|
| Behavior | Hard stop â retries if violated | Soft nudge â continues if violated |
| Use for | Must-comply rules (format, safety, legal) | Should-comply preferences (style, tone) |
| On failure | LM retries with error feedback | LM gets suggestion, continues |
| PM translation | “This must happen” | “This should happen” |
import dspy
# Hard rule â will retry up to max_backtrack_attempts times
dspy.Assert(
condition, # bool: does the output satisfy the rule?
"error message" # str: feedback to the LM on what went wrong
)
# Soft rule â nudges but doesn't block
dspy.Suggest(
condition,
"suggestion message"
)
Step 1: Identify your rules
Ask the user:
- What rules does the AI break? (too long? wrong format? forbidden content? missing fields?)
- Which rules are hard requirements vs nice-to-haves? (Assert vs Suggest)
- What should happen when a rule is broken? (retry, flag for review, fail loudly)
Step 2: Content policy rules
Enforce what the AI can and cannot say.
class PolicyCheckedResponse(dspy.Module):
def __init__(self):
self.respond = dspy.ChainOfThought("question -> answer")
def forward(self, question):
result = self.respond(question=question)
answer = result.answer
# Hard rules â must comply
dspy.Assert(
len(answer.split()) <= 280,
f"Response is {len(answer.split())} words. Must be under 280 words."
)
dspy.Assert(
not any(word in answer.lower() for word in BLOCKED_WORDS),
"Response contains blocked words. Remove them and regenerate."
)
dspy.Assert(
"disclaimer" not in answer.lower(),
"Do not include disclaimers. Answer directly."
)
# Soft rules â prefer but don't block
dspy.Suggest(
answer[0].isupper(),
"Response should start with a capital letter."
)
dspy.Suggest(
answer.endswith(".") or answer.endswith("!") or answer.endswith("?"),
"Response should end with proper punctuation."
)
return result
BLOCKED_WORDS = ["competitor_name", "profanity1", "profanity2"] # your list
Step 3: Format rules
Enforce output structure â valid JSON, required fields, correct types.
import json
from pydantic import BaseModel, Field
from typing import Literal
# Option A: Pydantic validation (automatic)
class QuizQuestion(BaseModel):
question: str = Field(min_length=10)
options: list[str] = Field(min_length=4, max_length=4)
correct_answer: str
difficulty: Literal["easy", "medium", "hard"]
class GenerateQuiz(dspy.Signature):
"""Generate a quiz question about the topic."""
topic: str = dspy.InputField()
quiz: QuizQuestion = dspy.OutputField()
# Option B: Assert-based validation (custom logic)
class QuizGenerator(dspy.Module):
def __init__(self):
self.generate = dspy.ChainOfThought(GenerateQuiz)
def forward(self, topic):
result = self.generate(topic=topic)
quiz = result.quiz
# Correct answer must be one of the options
dspy.Assert(
quiz.correct_answer in quiz.options,
f"Correct answer '{quiz.correct_answer}' is not in options {quiz.options}. "
"The correct answer must be one of the four options."
)
# Options must be unique
dspy.Assert(
len(set(quiz.options)) == 4,
"All four options must be different from each other."
)
return result
Combine Pydantic (catches type/structure errors) with Assert (catches logic errors) for the strongest format enforcement.
Step 4: Business constraint rules
Translate business requirements into programmatic constraints.
class PricingResponse(dspy.Module):
def __init__(self):
self.respond = dspy.ChainOfThought("customer_question, pricing_docs -> answer")
def forward(self, customer_question, pricing_docs):
result = self.respond(
customer_question=customer_question,
pricing_docs=pricing_docs,
)
# Never mention competitor pricing
dspy.Assert(
not any(comp in result.answer.lower() for comp in COMPETITORS),
"Do not mention competitor pricing. Focus only on our plans."
)
# Never offer unauthorized discounts
dspy.Assert(
"discount" not in result.answer.lower() or "authorized" in result.answer.lower(),
"Do not offer discounts unless referencing an authorized promotion."
)
# Always include a CTA
dspy.Suggest(
any(cta in result.answer.lower() for cta in ["contact", "sign up", "learn more", "get started"]),
"Include a call-to-action at the end of the response."
)
return result
COMPETITORS = ["competitor_a", "competitor_b"]
Step 5: How retry and backtracking works
When dspy.Assert fails, DSPy doesn’t just retry blindly â it feeds the error message back to the LM:
Attempt 1: LM generates response â Assert fails ("Response is 350 words, must be under 280")
Attempt 2: LM retries with feedback â Assert fails ("Response contains blocked words")
Attempt 3: LM retries with feedback â Assert passes â
Key details:
- Error messages matter. They’re the LM’s self-correction instructions. Be specific: “Response is 350 words, must be under 280” is better than “too long.”
- Default retries: 2. Set via
max_backtrack_attemptson the module. - Each retry sees all previous failures. The model gets a cumulative error log.
- Suggest never retries. It sends the feedback but continues regardless.
Step 6: Composing multiple rules
Stack rules by putting multiple Assert/Suggest calls in sequence. They’re checked in order.
class TweetWriter(dspy.Module):
def __init__(self):
self.write = dspy.ChainOfThought("topic, key_facts -> tweet")
def forward(self, topic, key_facts):
result = self.write(topic=topic, key_facts=key_facts)
tweet = result.tweet
# Rule 1: Length limit (hard)
dspy.Assert(
len(tweet) <= 280,
f"Tweet is {len(tweet)} chars. Must be â¤280."
)
# Rule 2: No hashtags (hard)
dspy.Assert(
"#" not in tweet,
"No hashtags allowed. Remove all # symbols."
)
# Rule 3: Must include key fact (hard)
dspy.Assert(
any(fact.lower() in tweet.lower() for fact in key_facts),
f"Tweet must mention at least one key fact: {key_facts}"
)
# Rule 4: Engaging tone (soft)
dspy.Suggest(
not tweet.startswith("Did you know"),
"Avoid starting with 'Did you know' â be more creative."
)
# Rule 5: No emojis (soft)
dspy.Suggest(
not any(ord(c) > 127 for c in tweet),
"Prefer text-only tweets without emojis."
)
return result
When rules conflict (e.g., “include all key facts” vs “stay under 280 chars”), put the harder constraint first so the model prioritizes it.
Step 7: Optimizing with rules
DSPy optimizers work with assertions. When you optimize a module that has Assert/Suggest:
- The optimizer sees assertion pass/fail rates as part of the metric
- Optimized prompts learn to satisfy constraints more often
- Result: fewer retries needed in production
def metric(example, pred, trace=None):
# Your quality metric + assertion compliance
correct = pred.answer == example.expected_answer
return correct # assertions are enforced separately during forward()
optimizer = dspy.MIPROv2(metric=metric, num_threads=4)
optimized = optimizer.compile(
my_module,
trainset=trainset,
max_bootstrapped_demos=4,
max_labeled_demos=4,
)
Key principles
- Assert for requirements, Suggest for preferences. Don’t use Assert for style issues.
- Specific error messages. “350 words, must be under 280” beats “too long.”
- Pydantic + Assert together. Pydantic catches structure, Assert catches logic.
- Order matters. Put hard constraints before soft ones.
- Optimize after adding rules. DSPy learns to comply, reducing runtime retries.
Additional resources
- Use
/ai-checking-outputsfor general output verification (safety, quality gates) - Use
/ai-stopping-hallucinationsfor grounding AI in facts and sources - Use
/ai-improving-accuracyto measure and improve quality after adding rules - Use
/ai-testing-safetyto verify your rules hold up against adversarial users - See
examples.mdfor complete worked examples