DEV Community

Deva
Deva

Posted on

Every post my engine wrote hit 200 characters. Here is the fix.

200 characters. That was the fixed soft target every post my engine generated aimed for. Replies had the 280 character hard cap and nothing else. The output looked fine on any given day, but zoom out and the pattern was obvious: the feed was metronomic. Every post the same length, every reply running close to the ceiling. A human's writing does not look like that.

The fix sounds simple: draw a random target per draft. But "random" is doing a lot of work there. I wanted most output to be short, punchy, one liner territory with only an occasional longer developed take. A uniform distribution would have produced too many medium length posts, which is arguably the worst outcome: not punchy enough to land as a one liner, not developed enough to carry a real argument.

So I went with a triangular distribution, mode at the minimum. For posts the range is 70 to 260 characters, mode at 70. For replies and conversation openers it is 20 to 150. The skew toward short means the typical draft is a one liner, and a longer draw is genuinely unusual rather than statistically normal.

Wiring it in was straightforward. There is a variance.target_chars(lo, hi) helper that does the triangular draw and returns the midpoint deterministically when variance is toggled off, which matters for tests and dry runs. Posts in generate.py, comments in comments.py, conversation openers and follow ups in conversations.py all call it and get a target injected into the prompt.

The trickier part was the prompt wording. The old generation prompt treated the char count as an upper bound: "aim for around X characters." With variance, a high draw needs to actually produce a longer tweet, not just set a ceiling the model ignores. I rewrote the framing to "write to approximately X characters" and validated that a draw near 260 actually produces a developed sentence or two rather than a 200 character draft with extra whitespace.

The other thing I had to add was a separate floor for replies. Posts already had a POST_MIN_CHARS=40 check so the critic gate could reject single word garbage. Replies can legitimately be very short now, so I added COMMENT_MIN_CHARS=15. Without that floor, a 20 character target might produce something the post gate would reject even though "sounds right" is a valid three word reply.

What I would do differently: I would have separated the distribution parameters from the code earlier. Right now VARIANCE_LENGTH_MODE_FRAC is tunable via environment variable but the lo/hi bounds are literals in each call site. That is fine for three call sites, but if this grows to more surfaces, a central config object would be cleaner. I also wish I had logged the per draft target alongside the draft text in the analytics database from day one. Right now I can see output length distribution but not whether the actual target tracked it, so I am eyeballing calibration rather than measuring it.

The feed already looks less robotic after a day of output. The variance is doing what variance is supposed to do: you notice it because it feels natural, not because it stands out.

Top comments (0)