通过模拟 flaky/slow/dead 三种端点故障,测试并改进 LLM 调用 Pipeline 的韧性,提供了可直接复用的 fault injection 代码。
Everyone blames the free model. I blamed my own code.
I pointed my CI at MonkeyCode's free model endpoint. It worked. Then I broke it on purpose.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The uncomfortable question
What happens when a model endpoint gets slow? Not down. Just slow.
Most pipelines have no answer. They wait. They retry. They stack.
I built a mock endpoint that injects faults. Then I watched my own pipeline panic.
Here's the mock. It simulates three failure modes.
# faulty_endpoint.py
import random, time
from flask import Flask, jsonify
app = Flask(__name__)
MODE = "flaky" # flaky | slow | dead
@app.route("/v1/chat/completions", methods=["POST"])
def chat():
if MODE == "dead":
return jsonify({"error": "boom"}), 500
if MODE == "slow":
time.sleep(8)
if MODE == "flaky" and random.random() < 0.3:
return jsonify({"error": "overloaded"}), 503
return jsonify({"choices": [{"message": {"content": "ok"}}]})
if __name__ == "__main__":
app.run(port=8080)
Install the deps and run it:
pip install flask
python faulty_endpoint.py
Now point any client at http://localhost:8080. No auth. No TLS. Just faults.
The three pipeline designs
I tested three ways to call an endpoint. Same faults. Same prompt.
resp = httpx.post(URL, json=PAYLOAD, timeout=30)
return resp.json()
One call. One timeout. No retry. Simple.
Design B: eager retry
for attempt in range(3):
try:
resp = httpx.post(URL, json=PAYLOAD, timeout=30)
return resp.json()
except Exception:
time.sleep(1)
Three attempts. One second apart. Feels robust.
try:
resp = httpx.post(URL, json=PAYLOAD, timeout=5)
return resp.json()
except Exception:
return fallback_summary()
Five-second timeout. Instant fallback. No retry.
I wrote a runner that fires 200 requests per design.
# run_experiment.py
import httpx, time
URL = "http://localhost:8080/v1/chat/completions"
PAYLOAD = {"messages": [{"role": "user", "content": "summarize"}]}
N = 200
def call(design):
t0 = time.perf_counter()
try:
if design == "naive":
r = httpx.post(URL, json=PAYLOAD, timeout=30)
return r.status_code, time.perf_counter() - t0
if design == "retry":
for _ in range(3):
try:
r = httpx.post(URL, json=PAYLOAD, timeout=30)
return r.status_code, time.perf_counter() - t0
except Exception:
time.sleep(1)
return 0, time.perf_counter() - t0
if design == "layered":
try:
r = httpx.post(URL, json=PAYLOAD, timeout=5)
return r.status_code, time.perf_counter() - t0
except Exception:
return 200, time.perf_counter() - t0
except Exception:
return 0, time.perf_counter() - t0
for design in ["naive", "retry", "layered"]:
results = [call(design) for _ in range(N)]
ok = sum(1 for s, _ in results if s == 200)
avg = sum(t for _, t in results) / N
print(f"{design}: ok={ok}/{N}, avg={avg:.2f}s")
pip install httpx
python run_experiment.py
Change MODE in the mock. Rerun. Compare.
Here are my numbers. Yours will differ. The shape won't.
Flaky mode (30% 503s)
Wait. The layered design "succeeded" in slow mode. How?
It didn't. It gave up at five seconds. The fallback returned a 200. The job stayed green.
The layered design "succeeded" again. Same trick. Fallback.
Design B looked robust. It was the most dangerous.
Here's why. Ten requests fail. Ten retries fire together. The endpoint sees a second wave. The retries fail too. Now you have twenty failures.
Retries without jitter are a stampede. Retries without a timeout budget are a time bomb.
In slow mode, Design B didn't retry. The first request succeeded after 8 seconds. But imagine a 30-second stall. Design B would wait 30, then 30, then 30. Ninety seconds for one summary.
The fallback that saved me
Design C's fallback was boring. A template.
def fallback_summary():
return {"content": "Updated files. See diff for details."}
No intelligence. No model. Just a sentence.
It saved every job that the endpoint couldn't handle. A dumb fallback beat a smart retry.
Why this matters for free endpoints
Free model servers are shared. Shared means noisy. Noisy means slow.
MonkeyCode's free model access and free server option are great for batch work. But they're shared infrastructure. Design for the flake.
You can't control the endpoint. You can control your timeout. You can control your fallback.
What I changed in my CI
Timeout at five seconds. Not thirty.
One retry with jitter. Not three.
A template fallback. Always.
The pipeline got faster. The failures got quieter. The endpoint never changed.
This is a mock, not the real endpoint. Real traffic has variance I didn't simulate.
The mock doesn't simulate network partitions. It doesn't simulate rate-limit headers. It doesn't simulate TLS handshake failures.
My fallback works for summaries. It won't work for code review or security analysis.
Run the mock yourself. Point your real client at it. Watch what breaks.
Skip if your pipeline is already resilient. Skip if you have no fallback path. Skip if you never fan out.
If you have a queue in front of the endpoint, the math changes. A queue absorbs bursts. My test had no queue.
That's the next experiment. Add a queue. Rerun the mock. Compare.
Everyone else should break their pipeline on purpose. Once. Before production does it for you.
For further actions, you may consider blocking this person and/or reporting abuse