Posted on
Artificial Intelligence

Prompt Chaining Explained

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Prompt Chaining Explained: Turn One Big Ask Into Reliable Bash Pipelines

Ever asked an LLM to “write a perfect backup script” and got something that looks right but fails on edge cases? That’s the classic “single prompt” pitfall. The fix isn’t magic model settings; it’s process. In Linux terms, think of prompt chaining as | for language models: you break a big job into small, reliable stages with checks in between. The result is higher accuracy, easier debugging, and repeatability you can trust.

This post explains what prompt chaining is, why it works, and how to do it from Bash. You’ll leave with a minimal CLI setup, a reusable llm function, and a practical chain that turns a plain-English requirement into a validated shell script.

What is Prompt Chaining (for Bash users)?

  • Conceptually: Prompt chaining is splitting a complex LLM task into multiple prompts where each step’s output feeds the next.

  • Analogy: Like grep | awk | sort | uniq, but with prompts. Each stage has a clear contract and produces structured data you can verify.

  • Goal: Higher reliability. Each step is simpler, has clearer expectations, and can be validated before you move on.

Why chaining works

  • Reduces cognitive load: Smaller prompts produce more focused, consistent outputs.

  • Enables validation: You can assert format, parse JSON, lint code, and fail fast.

  • Encourages reuse: You keep libraries of step prompts (e.g., “clarify spec,” “generate code,” “review risks”).

  • Improves debuggability: When something goes wrong, you know which step broke.

Prerequisites

We’ll call an OpenAI-compatible REST API from Bash using curl and parse outputs with jq. We’ll also lint generated scripts with shellcheck.

Install these packages with your package manager:

  • Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y curl jq shellcheck
  • Fedora/RHEL/CentOS (dnf):
sudo dnf install -y curl jq ShellCheck

Note: On RHEL/CentOS you may need EPEL first:

sudo dnf install -y epel-release
sudo dnf install -y ShellCheck
  • openSUSE Leap/Tumbleweed (zypper):
sudo zypper refresh
sudo zypper install -y curl jq ShellCheck

You’ll also need an API key for your provider. For OpenAI-compatible endpoints:

export OPENAI_API_KEY="sk-..."
# Optional overrides:
export LLM_MODEL="gpt-4o-mini"
export LLM_TEMPERATURE="0.2"
export LLM_BASE_URL="https://api.openai.com/v1/chat/completions"

A minimal Bash LLM client

Drop this into your shell or ~/.bashrc. It creates a small, composable function you can chain.

llm() {
  local sys="${1:-You are a careful, terse expert. Output only what is asked.}"
  local user="${2:-}"
  local model="${LLM_MODEL:-gpt-4o-mini}"
  local url="${LLM_BASE_URL:-https://api.openai.com/v1/chat/completions}"
  local key="${OPENAI_API_KEY:?Set OPENAI_API_KEY}"

  jq -n \
    --arg sys "$sys" \
    --arg user "$user" \
    --arg model "$model" \
    '{
      model: $model,
      temperature: (env.LLM_TEMPERATURE // "0.2" | tonumber),
      messages: [
        {role:"system", content:$sys},
        {role:"user",   content:$user}
      ]
    }' \
  | curl -sS \
      -H "Authorization: Bearer $key" \
      -H "Content-Type: application/json" \
      -d @- "$url" \
  | jq -r '.choices[0].message.content'
}

Optional validator for generated shell scripts:

validate_script() {
  local file="$1"
  echo "[*] Linting $file"
  shellcheck "$file" || { echo "[!] ShellCheck failed"; return 1; }
  bash -n "$file"   || { echo "[!] Bash syntax check failed"; return 1; }
  echo "[+] $file passed basic validation"
}

The 5-step playbook for prompt chaining

1) Decompose the task

  • Write down the stages before you touch the keyboard. For code generation:
    • Step A: Clarify and normalize the requirement into a structured spec.
    • Step B: Generate minimal, correct Bash given the spec.
    • Step C: Review risks and edge cases; patch holes.
    • Step D: Validate (lint, syntax, simple dry-runs).

2) Force structure with JSON

  • Ask the model to return compact JSON. It’s parseable, diffable, and testable.

  • Example system + user prompt for a “spec clarifier”:

SYS_SPEC="You extract clear, precise software specs. Respond only with compact JSON matching the schema."

REQ="Backup a directory to an external drive using rsync; exclude node_modules and .git; keep last 5 daily snapshots with hardlinks; log to syslog; handle spaces in paths."

SPEC=$(
  llm "$SYS_SPEC" "$(cat <<'EOF'
Schema (JSON keys only):

- name: short string

- description: one sentence

- inputs: array of {name, type, description}

- outputs: array of {name, description}

- constraints: array of strings

- edge_cases: array of strings

- test_plan: array of short commands or checks

Text:
EOF
)
$REQ"
)
echo "$SPEC" | jq .

3) Implement the chain in Bash

  • Feed the structured spec into a “code generator” prompt that outputs only the script (no prose, no backticks). Then save and validate.
SYS_CODE="You write safe, portable Bash. Respond with only the script content, no explanations or backticks."

SCRIPT=$(llm "$SYS_CODE" "$(cat <<EOF
Write a POSIX-compliant Bash script that implements the following JSON spec.
Rules:

- Be defensive: 'set -euo pipefail'; quote variables; handle spaces.

- Use rsync safely; create dated snapshots; keep only the last 5.

- Log via logger.

- Provide usage/help.

- Output only the script, no markdown/no backticks.

Spec:
$SPEC
EOF
)")

printf '%s\n' "$SCRIPT" > backup.sh
chmod +x backup.sh
validate_script backup.sh

4) Add a review gate

  • Ask the model to critique the script based on the spec and common shell pitfalls. If it finds issues, request a minimal patch.
SYS_REVIEW="You are a paranoid shell auditor. Respond in compact JSON with fields: risks[], improvements[], one_liner_patch (unified diff if needed)."

REVIEW=$(
  llm "$SYS_REVIEW" "$(cat <<'EOF'
Given the JSON spec and the Bash script below, find security, portability, or data-loss risks.

- Focus on quoting, rsync flags, trap/cleanup, log clarity, and retention logic.

- If a change is needed, produce a minimal unified diff patch; otherwise set one_liner_patch to "".

EOF
)
Spec:
$SPEC

Script:
$(cat backup.sh)
"
)
echo "$REVIEW" | jq .
  • If one_liner_patch contains a diff, apply it (example assumes git apply is available; otherwise, write a small patcher):
PATCH=$(echo "$REVIEW" | jq -r '.one_liner_patch // ""')
if [ -n "$PATCH" ] && [ "$PATCH" != "null" ]; then
  printf '%s\n' "$PATCH" > /tmp/patch.diff
  git init -q >/dev/null 2>&1 || true
  git add backup.sh || true
  git apply /tmp/patch.diff || { echo "[!] Patch failed"; exit 1; }
  validate_script backup.sh
fi

5) Log, cache, and iterate

  • Save every step so you can diff and reproduce:
run_id=$(date +%Y%m%d-%H%M%S)
mkdir -p chain_logs/"$run_id"
printf '%s\n' "$REQ"    > chain_logs/"$run_id"/01_request.txt
printf '%s\n' "$SPEC"   > chain_logs/"$run_id"/02_spec.json
printf '%s\n' "$SCRIPT" > chain_logs/"$run_id"/03_script.sh
printf '%s\n' "$REVIEW" > chain_logs/"$run_id"/04_review.json
  • Tweaking rules or adding validation (e.g., quick dry-run with a temp directory) is now easy:
TESTDIR=$(mktemp -d)
mkdir -p "$TESTDIR/src" "$TESTDIR/dst"
echo "demo" > "$TESTDIR/src/file with spaces.txt"
# Example dry-run if your script supports it (adjust flags accordingly)
./backup.sh --source "$TESTDIR/src" --target "$TESTDIR/dst" --dry-run || true
rm -rf "$TESTDIR"

Real-world example: From prose to a safer rsync snapshotter

  • Step A (clarify) extracts a JSON spec reflecting exclusions, retention, and logging.

  • Step B (generate) produces a POSIX shell script with set -euo pipefail, safe quoting, and a help message.

  • Step C (review) flags missing -- separators, unsafe globbing, or incorrect rsync options before you ever run it on real data.

  • Step D (validate) runs shellcheck and a syntax check, then a harmless dry-run in a temp directory.

This beats the “one giant prompt” because each stage is auditable and can fail fast. If retention policy is wrong, you’ll see it in 02_spec.json. If quoting is off, shellcheck shouts before data touches disks.

Tips and gotchas

  • Be explicit: Ask for “only JSON” or “only the script, no backticks.” Enforce with parsing.

  • Keep temperature low for deterministic chains: export LLM_TEMPERATURE=0.2.

  • Abort on parse or lint failures. Don’t pass bad outputs downstream.

  • Red-team your own chain: Intentionally create ambiguous requirements and see if Step A catches them.

Conclusion and Call to Action

Prompt chaining makes LLMs feel like Unix: small, composable steps with strong contracts. Start with the skeleton above and adapt it to your workflow—code generation, doc summarization, config hardening, or log analysis. Your next step:

  • Install the tools:

    • apt:
    sudo apt update
    sudo apt install -y curl jq shellcheck
    
    • dnf:
    sudo dnf install -y curl jq ShellCheck
    

    For RHEL/CentOS if needed:

    sudo dnf install -y epel-release
    sudo dnf install -y ShellCheck
    
    • zypper:
    sudo zypper refresh
    sudo zypper install -y curl jq ShellCheck
    
  • Export your API key and add the llm function to your shell.

  • Build your first 3-step chain: clarify → generate → review, with a validator in between.

When you’ve got it working, save your best prompts as snippets and share them with your team. Small, reliable chains beat single-shot hero prompts every day of the week.