Blog /
From thumbs-up to better SQL: turning user feedback into verified examples
Published . Written for nlqueries-core 0.4.
feedback accuracy security
A text-to-SQL feedback loop turns "that answer was right" into better SQL for the next person who asks something similar. The idea is simple: keep the question-and-SQL pairs people confirm, and show the nearest ones to the model as examples. The hard part is that an example steers every later answer on that agent, for everyone, so the loop is also a way to put words in the model's mouth. This post describes the loop in nlqueries-core 0.4: the two kinds of feedback, how each one reaches the prompt, and the checks a pair has to pass on the way.
Two kinds of feedback, two paths
| You record | It becomes | Where the model sees it | Needs Qdrant |
|---|---|---|---|
| A thumbs-up, with the SQL | A verified pair, via promote-feedback |
The nearest pairs to each question | Yes |
| A thumbs-down, with corrected SQL | A correction in the knowledge base, via export-kb |
At the front of the example queries, for every question | No |
Verified pairs are retrieved by meaning: for each question, up to three stored
pairs whose questions are close to it (cosine similarity of 0.75 or more) are
added to the prompt's example queries. They go first, ahead of the examples
mined from query history, and are labelled -- verified example. Corrections
go into the knowledge base itself, ahead of the mined examples, and the model
sees the first few of those on every question.
Recording feedback
From the command line:
nlqueries feedback shop --question "Revenue by region last quarter" --thumbs-up \
--generated-sql "SELECT region, SUM(amount) FROM orders WHERE ... GROUP BY region"
nlqueries feedback shop --question "Customers with no orders" --thumbs-down \
--corrected-sql "SELECT c.id FROM customers c LEFT JOIN orders o ON o.customer_id = c.id WHERE o.id IS NULL"
Pass the SQL with a thumbs-up. --generated-sql is optional, and a thumbs-up
recorded without it is stored, but promotion needs a pair: a record with no SQL
is passed over without a word. nlqueries query --json prints the generated
SQL to copy from. A thumbs-up that also carries --corrected-sql promotes the
corrected statement.
A correction is saved, not applied. It reaches the knowledge base on the next
export-kb. nlqueries feedback-stats shop counts what you have recorded so
far.
The MCP server has a feedback tool too, so an assistant can rate an answer in the conversation. Those records are kept, but the loop does not use them unless you ask it to. The next section explains why.
Why only feedback that names someone
A promoted pair is shown to the model for every later question it resembles, on that agent, for every user. Whoever chose the pair chose part of everyone's prompt. So the rating has to come from someone who can be held to it.
Each record carries its origin. A record made with nlqueries feedback was run
by an operator on the host, and counts. A record from the MCP tool identifies
nobody: it is whatever the client sent. One written from Python with
record_feedback counts only if your code sets its source; left at the
default, its origin is unknown. Neither promote-feedback nor export-kb uses
records of unknown origin, MCP records, or records from before origins were
tracked, unless you pass --include-anonymous. Read them first:
nlqueries promote-feedback shop --include-anonymous
nlqueries export-kb shop --include-anonymous
Both commands say how many records they skipped and why, so nothing is passed over in silence, apart from the thumbs-up with no SQL above.
The checks a pair has to pass
Promotion and corrections apply the same checks, in the same order. A pair is kept only when:
- its SQL passes the SQL policy in the agent's dialect, as a single read-only query, the same test a generated statement faces before it runs;
- it names at least one table, and only tables in the knowledge base. A
table named with a schema has to match the schema the knowledge base records,
so
other.ordersdoes not pass forpublic.orders; - the knowledge base has a schema to check against. With none (an agent
never exported, say), nothing is promoted, and the warning says to run
export-kbfirst.
What is stored is normalised too. The question is kept on one line of at most 200 characters, and the SQL without comments. Both end up in the prompt, where a line break in the question could end the line it sits on and turn the rest of the text into instructions, and a comment in the SQL would be shown as written. In a correction, literals also become typed placeholders, as in the examples mined from history, so the example shows the shape of the query rather than the values someone typed.
These checks are why a pair that passes is safe to show. They do not decide whether its SQL answers its question. That is what the rating is for, and why it matters who gave it.
Pairs that no longer fit are removed
Schemas change. After promoting, promote-feedback checks every pair already
stored for the agent against the same rules and deletes the ones that fail:
a pair whose table has left the knowledge base, or one promoted by an earlier
release under looser rules. The command reports how many it removed. A removed
pair comes back on a later promotion if its feedback qualifies by then.
There is one exception. If every stored pair fails at once, nothing is deleted and a warning is logged. That points at the check rather than the pairs: for example, an engine the SQL parser has no grammar for is read as PostgreSQL, and then no pair parses at all.
Signed, so Qdrant cannot add its own
Verified pairs live in Qdrant, in agent_<id>_verified, and anything that can
write to Qdrant could add a pair. So in 0.4, promote-feedback signs each pair
with the semantic-cache signing key, which is held outside Qdrant. The reader
uses only pairs whose signature verifies and whose agent is the one asking.
It fetches four times as many candidates as it needs, so a few unsigned pairs
near the question do not push the real ones out. More than that leaves the
prompt with fewer verified examples, never with planted ones. Every process
that reads the pairs needs the key that signed them: set
NLQ_CACHE_SIGNING_KEY where more than one process shares a Qdrant. See
configuration.
If you are upgrading from 0.3, pairs promoted then are unsigned and ignored,
with a warning, until you run promote-feedback (or export-kb) once per
agent.
The loop, in commands
nlqueries query shop "Revenue by region last quarter" --json # copy the SQL
nlqueries feedback shop --question "Revenue by region last quarter" \
--thumbs-up --generated-sql "<the SQL>"
nlqueries feedback shop --question "..." --thumbs-down --corrected-sql "<the fix>"
nlqueries export-kb shop # folds in corrections, then promotes thumbs-ups
nlqueries promote-feedback shop # between exports, after a batch of ratings
export-kb runs the promotion step at the end of every run, without
--include-anonymous, so most of the time it is the only command you need.
promote-feedback is for a batch of ratings between exports. Verified pairs
need a reachable Qdrant and the embedding model; corrections need neither,
since they live in the knowledge-base file. Qdrant setup
covers the first.
In short
- A thumbs-up needs its SQL, or there is nothing to promote.
- Only feedback that names someone is used, unless you review the rest.
- A pair must pass the SQL policy and read only known tables, and it is deleted when it stops doing so.
- Pairs are signed, so a writer to Qdrant cannot add one.
The commands are in the CLI reference under
feedback and
promote-feedback. For the examples the
model gets from your query history, which these sit beside, see
How to build a text-to-SQL knowledge base from query history.
To try it, pip install nlqueries-core and start with
getting started.