Skip to content

When the documents do not know ​

Task: when the documents do not cover a question, say so — instead of letting the model answer from memory.

yaml
# Recipe: when the documents do not cover the question, say so — do not guess.
paths:
  components: ./components

llm:
  model: phi3

The same three documents as in Answers with their sources. Without minRelevance, the nearest chunks always come back, related to the question or not. With it, a chunk less relevant than the floor is dropped; when none is left, answer_result.found is false and the model is never called.

xml
<q:component name="Ask">
  <q:knowledge name="docs" persist="false" chunkSize="300" chunkOverlap="30">
    <q:source type="directory" path="knowledge" pattern="*.md" />
  </q:knowledge>

  <q:set name="question" value="{query.q}" default="" />

  <q:if condition="question">
    <!-- minRelevance: chunks less relevant than 0.8 are not retrieved. When
         none remains, the model is not asked at all — it would answer from
         memory — and answer_result.found is false. The right floor depends
         on the embedding model and the chunk size: look at the relevance of
         a few sources before choosing it. -->
    <q:llm name="answer" knowledge="docs" top="2" minRelevance="0.8">
      <q:message role="user">{question}</q:message>
    </q:llm>
  </q:if>

  <ui:window title="Ask the store">
    <ui:form>
      <ui:input bind="q" value="{question}" placeholder="Your question" />
      <ui:button variant="primary">Ask</ui:button>
    </ui:form>
    <q:if condition="question">
      <q:if condition="answer_result.found">
        <ui:text>{answer}</ui:text>
        <q:loop items="{answer_result.sources}" var="s">
          <ui:text>[{s.n}] {s.name} ({round(s.relevance, 2)})</ui:text>
        </q:loop>
        <q:else>
          <ui:alert variant="info">Our documents do not answer that. Write to us at help@example.com.</ui:alert>
        </q:else>
      </q:if>
    </q:if>
  </ui:window>
</q:component>
xml
<!-- Structural checks, never the model's exact words: the same tests run
     against a real model before every release. -->
<q:test name="a question the documents answer" page="/">
  <test:visit q="How many days do I have to return an order?" />
  <test:expect text="[1] returns.md" />
  <test:expect no-text="do not answer that" />
</q:test>

<q:test name="a question they do not answer" page="/">
  <test:visit q="What is the capital of France?" />
  <test:expect text="Our documents do not answer that." />
  <test:expect no-text="[1]" />
</q:test>
text
tests/honest.test.q
  PASS  a question the documents answer
  PASS  a question they do not answer
2 passed, 0 failed

The floor depends on the embedding model and the chunk size. Print s.relevance for a few questions your documents answer, and a few they do not, and put the floor between them — the page above shows it next to each source for that reason.

Tested: in CI these tests run against a stand-in model server, which answers from the first source it is given; before every release they run against a real model (tests/live_ai/test_cookbook_ai.py). That is why they check structure — which source, which tool, what the page shows on failure — and never the model's words.

See IA-9.

MIT Licensed · Built with VitePress