---
title: AI-karakterek
url: 'https://wiki.teletypegames.org/hu/engines/rubbs/ai'
markdown: 'https://wiki.teletypegames.org/hu/engines/rubbs/ai.md'
lang: hu
date: '2026-10-27'
description: 'A rubbs BBS::AI modulja (forrás: lib/bbs/ai.rb, 0.5.0 óta) olyan karaktereket ad, akikkel a hívó beszélgethet: hogy ki a karakter, az egy YAML-fájlban (a personában) van, a beszédet egy kis modell végzi bármilyen OpenAI-kompatibilis chat-végpont mögött, hívónként rövid memória tartja a beszélgetés…'
taxonomy:
  tag:
    - reference
---

# AI-karakterek

> A [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs) `BBS::AI` modulja (forrás: `lib/bbs/ai.rb`, 0.5.0 óta) olyan karaktereket ad, akikkel a hívó beszélgethet: hogy ki a karakter, az egy YAML-fájlban (a personában) van, a beszédet egy kis modell végzi bármilyen OpenAI-kompatibilis chat-végpont mögött, hívónként rövid memória tartja a beszélgetést, és beégetett válaszok ugranak be, ha a modell nem elérhető vagy elhasal. Ez az oldal sorra veszi az összes osztályt és kulcsot, megmutatja, hogyan kerül egy karakter doorba vagy window-ba, és hogy éles boardjaink hogyan bánnak a kis modellel. Minden példát a rubbs 0.17.0-n futtattunk, offline vagy egy helyi csonk ellen, valódi modell nélkül. Az API az 1.0.0-ban is ugyanez; a 0.33.0 óta a kliensnek határideje, válaszkorlátja és az egyszerre futó kérésekre vonatkozó korlátja is van (lásd *Client*).

# Az elemek

| Osztály | Mi ez |
|---|---|
| `BBS::AI::Persona` | akit a modell játszik, adatként: system prompt, példakörök, kérésopciók, deflect-szabályok, beégetett válaszok |
| `BBS::AI::Client` | streamelő kliens a `/v1/chat/completions`-hoz (llama.cpp `llama-server`, Ollama, vLLM, hosztolt API) |
| `BBS::AI::Chat` | egy persona, ami egy kliensen át beszél, hívónként rövid előzménnyel a hívó contextjében |
| `BBS::AI::Typewriter` | a streamelt szöveget egy FakeOS-konzolra gépeli, prefix alá tördelve |

Így dönti el a `Chat#reply`, mit mondjon:

```mermaid
flowchart TD
  Q[a hívó szövege] --> D{illik rá deflect-szabály?}
  D -- igen --> DR[a szabály válasza, modellhívás nélkül]
  D -- nem --> E{be van kapcsolva a kliens? nem üres az URL}
  E -- nem --> F
  E -- igen --> M[streamelés a modellből, darabonként]
  M --> R{jött szöveg?}
  R -- igen --> OK[a modell válasza, bekerül az előzménybe]
  R -- nem, vagy hibás volt a kérés --> F{van fallback: argumentum?}
  F -- igen --> FB[a megadott fallback]
  F -- nem --> P[a persona beégetett válasza: az első illeszkedő szabály, különben a default]
```

Egy karakter modell nélkül is működik: üres URL-lel a chat sosem hív modellt, és a beégetett válaszokból felel, így egy board már azelőtt kiadhatja a karaktert, hogy lenne modellje.

# Persona

## A YAML-fájl

```yaml
# The Oracle of the board, read once when the BBS starts.
system: |
  You are the Oracle, an old terminal in the back room of a 1985 bulletin board.
  You speak to one caller at a time, in one or two short sentences.
  You know the board and the night; you know nothing after 1985.

examples:
  - user: hi
    assistant: I am here, caller. The line is quiet tonight.
  - user: who are you?
    assistant: The Oracle, the oldest machine on this board. I hum, and sometimes I know things.

options:
  max_tokens: 80
  temperature: 0.4
  stop: ["\n\n", "```"]

deflect:
  - match: '\b(code|python|javascript|sql)\b'
    reply: I do not write code, caller. I only read the stars on the screen.
  - match: '\b(ai|chatbot|language model)\b'
    reply: Those words mean nothing to me. I am the Oracle.

fallback:
  replies:
    - match: '\b(hello|hi|hey)\b'
      reply: I am here, caller.
    - match: '\bweather\b'
      reply: '"{input}" — the sky is dark, as always at this hour.'
  default: 'The Oracle hums and says nothing about "{input}".'
```

| Kulcs | Típus | Alapérték | Jelentés |
|---|---|---|---|
| `system` | String | `''` | a system prompt; az elejéről és végéről levágja a szóközöket, a belső sortöréseket megtartja. Üresen nincs system üzenet |
| `examples` | `user` / `assistant` párok listája | `[]` | körök a megfelelő hangon, minden kérésben a system prompt után és az előzmények előtt |
| `options` | Hash | `{}` | a kéréstörzs további mezői: `max_tokens`, `temperature`, `top_p`, `stop`, … bármi, amit a végpont elfogad |
| `deflect` | `match` / `reply` párok listája | `[]` | **modell nélkül** megválaszolt kérdések, elsőként próbálva: amire egy kis modell akárhogy kérdezik, kiesik a szerepéből |
| `fallback.replies` | `match` / `reply` párok listája | `[]` | beégetett válaszok arra az esetre, ha a modell nem elérhető vagy elhasal, fentről lefelé próbálva |
| `fallback.default` | String | `''` | a beégetett válasz, ha egyik szabály sem illik |

- A `match` Ruby reguláris kifejezés, mindig kis- és nagybetűtől függetlenül. YAML-ben szimpla idézőjelek közé írd, hogy a `\b` backslash maradjon.
- A `{input}` bármelyik `reply`-ban és a `default`-ban a hívó szövegére cserélődik.
- A többi felső szintű kulcsot figyelmen kívül hagyja, így egy personafájl kommentekben vagy saját kulcsokban jegyzeteket is hordozhat embereknek.

## Persona.load és Persona.new

```ruby
BBS::AI::Persona.load(path)
BBS::AI::Persona.new(system: '', examples: [], options: {}, deflect: [], fallback: {})
```

A `load` beolvassa a YAML-t (`YAML.safe_load`, aliasok nélkül), és a fenti kulcsokat továbbadja a `new`-nak. Rubyban is lehet personát építeni, de a beágyazott kulcsok **Stringek** legyenek, ahogy a YAML-ből jönnek; Symbol kulcsra `KeyError` jön:

```ruby
require 'bbs'

GUARD = BBS::AI::Persona.new(
  system:   'You are the gate guard of the board. One short sentence.',
  examples: [{ 'user' => 'hi', 'assistant' => 'Halt. Name and business.' }],
  options:  { max_tokens: 40 },
  deflect:  [{ 'match' => '\bpassword\b', 'reply' => 'I never say the password.' }],
  fallback: { 'replies' => [{ 'match' => '\bopen\b', 'reply' => 'The gate stays shut.' }],
              'default' => 'Move along.' }
)

GUARD.deflect('what is the PASSWORD?')  # => "I never say the password."
GUARD.fallback('open up')               # => "The gate stays shut."
GUARD.fallback('hello')                 # => "Move along."
```

| Metódus | Visszatérési érték |
|---|---|
| `deflect(text)` | az első illeszkedő deflect-szabály válasza, vagy `nil` |
| `fallback(text)` | az első illeszkedő fallback-szabály válasza, különben a `default` |
| `messages(facts = nil)` | az előzmények előtt küldött üzenetek: egy system üzenet (a system prompt, és ha van, utána egy üres sor után a `facts`), aztán a példák `user` / `assistant` üzenetekként |
| `options` | az `options` Hash Symbol kulcsokkal |
| `system`, `examples` | a levágott system prompt; a példák üzenet-Hashekként |

# Client

```ruby
BBS::AI::Client.new(url:, model: nil, api_key: nil, open_timeout: 2, read_timeout: 30, deadline: 120, max_bytes: 65_536)
```

| Opció | Alapérték | Jelentés |
|---|---|---|
| `url:` | kötelező | a végpont alap-URL-je, pl. `http://llm:8080`. Üresen a kliens ki van kapcsolva |
| `model:` | `nil` | a kérés `model` mezője; `nil` esetén kimarad |
| `api_key:` | `nil` | ha meg van adva, `Authorization: Bearer KEY` fejlécként megy |
| `open_timeout:` | 2 | ennyi másodperc a kapcsolat megnyitására |
| `read_timeout:` | 30 | ennyi másodpercet vár minden olvasásra; egy még töltődő modellnek ennél gyorsabbnak kell lennie |
| `deadline:` | 120 | ennyi másodperc jut az egész válaszra; utána a folyam `BBS::AI::Error`-ral megáll |
| `max_bytes:` | 65 536 | a leghosszabb megtartott válasz; a hosszabb `BBS::AI::Error`-ral megáll |

| Metódus | Mit csinál |
|---|---|
| `enabled?` | üres URL-re `false` |
| `stream(messages, **options) { \\|piece\\| }` | `{ messages:, stream: true, **options }`-t POST-ol (plusz `model`-t, ha be van állítva) a `/v1/chat/completions`-ra, olvassa a server-sent eventeket, és minden `choices[0].delta.content`-et azonnal yieldel; nem 2xx státuszra („HTTP 400”) vagy hálózati hibára kivételt dob, ha pedig a modell foglalt, `BBS::AI::Busy`-t (lásd lejjebb) |
| `url` | a kapott URL |

A kliens maga nem olvas környezeti változót; a board dönti el, honnan jönnek a beállítások. Nálunk karaktercsaládonként van egy prefix: a SERMO a `SERMO_LLM_URL`, `SERMO_LLM_MODEL` és `SERMO_LLM_API_KEY` változókat olvassa; a Teletype BBS saját karakterei a `BBS_LLM_URL`, `BBS_LLM_MODEL` és `BBS_LLM_API_KEY` változókat, és ahol valamelyik nincs beállítva, a SERMO értékét veszik (`lib/characters/llm.rb` a `bbs/teletype-bbs`-ben):

```ruby
require 'bbs'

module Characters
  def self.env(name)
    own = ENV["BBS_LLM_#{name}"].to_s
    own.empty? ? ENV.fetch("SERMO_LLM_#{name}", nil) : own
  end

  def self.client
    BBS::AI::Client.new(url: env('URL').to_s, model: env('MODEL'), api_key: env('API_KEY'))
  end
end
```

**Egy modell, sok hívó.** Az egész folyamatban egyszerre legfeljebb `BBS.config.ai_concurrency` kérés (alapból 2) fut, bármelyik kliens küldi is; a következő legfeljebb 5 s-ot vár a sorára, aztán `BBS::AI::Busy`-t kap („The model is busy, try again in a moment.”). A `Chat` erre a persona tartalékválaszával felel, mint amikor a modell nem elérhető, így az orákulum előtt tolongó hívók nem halmoznak fel szálakat egy kis modell mögött. Állítsd arra, amennyit a modellszerver párhuzamosan ki tud szolgálni:

```ruby
BBS.configure { |c| c.ai_concurrency = 4 }
```

Amit a végpontokról tudni kell:

- Az útvonal mindig `/v1/chat/completions` a hoszt gyökerétől: az `URI.join` eldobja az `url:` útvonalát. Egy útvonal-prefix alatt élő végponthoz (`https://example.com/openai/v1/...`) proxy vagy prefix nélküli URL kell.
- A router módú llama.cpp-szervernek (több modell egy port mögött) nincs alapértelmezett modellje: a `model` nélküli kérés HTTP 400-at kap. Ott mindig add meg a `model:`-t.
- A közös modellszolgáltatásunk ilyen router: llama.cpp OpenAI-kompatibilis végponton, a Docker-hálózatunkon belül `http://llm:8080`, többek között a `qwen3.5-0.8b` modellel; hívónként saját kulcsot kér. A kulcs a környezetben legyen, sosem a personafájlban vagy a repóban:

```yaml
services:
  bbs:
    environment:
      - SERMO_LLM_URL=http://llm:8080
      - SERMO_LLM_MODEL=qwen3.5-0.8b
      - SERMO_LLM_API_KEY=${SERMO_LLM_API_KEY:-}
```

# Chat

```ruby
BBS::AI::Chat.new(persona:, client:, history: 6, key: :ai_history, filter: PLAIN_TEXT, options: {})
```

| Opció | Alapérték | Jelentés |
|---|---|---|
| `persona:` | kötelező | egy `Persona` |
| `client:` | kötelező | egy `Client`, vagy bármilyen objektum `enabled?` és `stream(messages, **options)` metódussal |
| `history:` | 6 | hány korábbi üzenetet (a user és az assistant is számít) küld el; a 2 az utolsó kör, a 0 semmi |
| `key:` | `:ai_history` | a hívó context hashének melyik kulcsán él az előzmény; minden karakternek adj sajátot |
| `filter:` | `Chat::PLAIN_TEXT` | regexp azokra a karakterekre, amiket minden streamelt darabból kiszed; `nil` mindent megtart |
| `options:` | `{}` | kérésopciók, amik felülírják a personáéit (például egy környezeti változóból jövő `max_tokens`) |

A `Chat::PLAIN_TEXT` azt szedi ki, amit egy sima szöveges terminál nem tud megjeleníteni: a `*`, `#` és backtick markdown-jeleket, az emojikat és az emojikat összefűző karaktereket.

## reply

```ruby
chat.reply(text, ctx = {}, facts: nil, fallback: nil) { |piece| }
```

Stringként adja vissza a választ. Blokkal a választ érkezés közben, darabonként is yieldeli; a deflect- vagy beégetett választ egyben, egyszer. Sorrendben:

1. A `text`-et UTF-8-ra kényszeríti, az érvénytelen bájtokat eldobja, a szóközöket levágja.
2. Az illeszkedő `deflect`-szabály azonnal válaszol; a modellt nem kérdezi, és az előzménybe sem kerül semmi.
3. Ha a kliens be van kapcsolva, a kérés: a persona üzenetei (a `facts`-szal), aztán az előzmények a `ctx[key]`-ből, aztán a hívó szövege. Minden streamelt darab átmegy a `filter`-en; az üres darabokat kihagyja.
4. A szöveget tartalmazó válasz visszamegy és megmarad: a kérdés és a válasz a `ctx[key]` végére kerül, amit az utolsó `history` darab üzenetre vág. A félúton hibával megszakadt választ úgy tartja meg, ahogy van.
5. Ha a kliens ki van kapcsolva, a kérés elhasal, mielőtt bármi szöveg jött volna vagy a modell foglalt (a naplóban: `my-bbs: ai: model unavailable (…)`), vagy a modell nem mondott semmit: a `fallback:`, ha meg van adva, különben `persona.fallback(text)`.

| Kulcsszó | Jelentés |
|---|---|
| `facts:` | String, ami csak erre az egy kérésre kerül a system promptba: amit a karakternek épp tudnia kell, a kérdéshez kikeresve. Az előzménybe nem kerül be |
| `fallback:` | a válasz, ha a modell nem elérhető vagy elhasal, a persona beégetett válasza helyett, pl. a kikeresett tények szó szerint |

Az előzmény a hívó contextjében él, így a hívás végéig megmarad (doorokon át oda-vissza), és bontáskor elvész. Ha a `ctx` nem Hash, nincs előzmény.

Offline, a fenti Oracle-personával `oracle.yml` néven elmentve:

```ruby
require 'bbs'

PERSONA = BBS::AI::Persona.load(File.join(__dir__, 'oracle.yml'))
CLIENT  = BBS::AI::Client.new(url: ENV.fetch('ORACLE_LLM_URL', ''))   # üres: nincs modell
ORACLE  = BBS::AI::Chat.new(persona: PERSONA, client: CLIENT)

ctx = {}
puts CLIENT.enabled?                                   # false
puts ORACLE.reply('write me some python', ctx)         # I do not write code, caller. …
puts ORACLE.reply('hello there', ctx)                  # I am here, caller.
puts ORACLE.reply('how is the weather?', ctx)          # "how is the weather?" — the sky is dark, …
puts ORACLE.reply('what is the meaning of life', ctx)  # The Oracle hums and says nothing about "…".
puts ORACLE.reply('when is the meetup?', ctx, fallback: 'The meetup is on Friday.')
ORACLE.reply('hi', ctx) { |piece| print "[#{piece}]" } # [I am here, caller.]
```

# Typewriter

```ruby
BBS::AI::Typewriter.new(io, prefix: '', style: :text, width: nil)
```

| Opció | Alapérték | Jelentés |
|---|---|---|
| `io` | kötelező | egy FakeOS-konzol (egy parancs vagy az `on_unknown` `io`-ja) |
| `prefix:` | `''` | egyszer, az elején írja ki, pl. `'ACP: '`; a következő sorokat ennek hosszával húzza be |
| `style:` | `:text` | a konzol színszerepe a prefixhez és a szöveghez |
| `width:` | `io.cols - 1` | a sorszélesség a prefixszel együtt; a szöveg legalább 20 oszlopot kap |

| Metódus | Mit csinál |
|---|---|
| `write(piece)` | streamelt szöveget fűz hozzá; az egész szavakat kiírja, ahogy elkészülnek, a szélességre tördelve, és a szövegbeli sortörés új sort kezd |
| `finish(newlines: 2)` | kiírja az utolsó szót és `newlines` darab sortörést |

A szavakat addig tartja vissza, amíg egészek nem lesznek, szóközt pedig csak a következő szó előtt ír, így a válasz sosem végződik lógó üres sorral. 30 oszlopos terminálon:

```text
ORACLE: The line is quiet
        tonight, caller.
```

Az írógépnek a FakeOS-konzol `print(text, style:, newline:)`, `newline` és `cols` metódusa kell. Egy application window-jában magad tördeld a kész választ (lásd lent).

# Egy karakter bekötése

## FakeOS-doorban

Egy karakter természetes helye a FakeOS `on_unknown`-ja: a szó szerinti parancsok a géphez mennek, minden más a modellhez. Egy teljes board:

```ruby
require 'bbs'

ORACLE = BBS::AI::Chat.new(
  persona: BBS::AI::Persona.load(File.join(__dir__, 'oracle.yml')),
  client:  BBS::AI::Client.new(url:     ENV.fetch('ORACLE_LLM_URL', ''),
                               model:   ENV['ORACLE_LLM_MODEL'],
                               api_key: ENV['ORACLE_LLM_API_KEY']),
  key:     :oracle_history,
  history: 2
)

ORACLE_OS = BBS::FakeOS.define do
  name   'ORACLE'
  prompt '? '
  colors text: "\e[0;36m", dim: "\e[2;36m", prompt: "\e[1;36m"

  boot do
    cls
    banner 'THE ORACLE'
    puts 'Ask anything. EXIT goes back to the board.', style: :dim
    newline
  end

  command 'exit', 'bye' do |_args, io|
    io.halt
  end

  on_unknown do |input, io|
    io.newline
    out = BBS::AI::Typewriter.new(io, prefix: 'ORACLE: ', style: :dim)
    ORACLE.reply(input, io.ctx) { |piece| out.write(piece) }
    out.finish
  end
end

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    os ORACLE_OS
  end
end

BBS.start
```

`ORACLE_LLM_URL` nélkül a beégetett válaszokból felel; `ORACLE_LLM_URL=http://llm:8080 ORACLE_LLM_MODEL=qwen3.5-0.8b ORACLE_LLM_API_KEY=YOUR_KEY` mellett a modellből streamel. Doorként egy applicationben: `door :oracle, ORACLE_OS, label: '&Oracle'` (lásd [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors)).

## Egy application window-jában

Egy window nem várhat a modellre: az eseményciklus másodpercekre megfagyna. Kérdezz egy szálban, a kész sorokat tedd egy sorba (Queue), és egy `Stream` window a tétlenségi tickjén (nagyjából másodpercenként) szedje fel őket:

```ruby
require 'bbs'

ORACLE = BBS::AI::Chat.new(
  persona: BBS::AI::Persona.load(File.join(__dir__, 'oracle.yml')),
  client:  BBS::AI::Client.new(url: ENV.fetch('ORACLE_LLM_URL', ''), model: ENV['ORACLE_LLM_MODEL']),
  key:     :oracle_history
)

BOARD = BBS::Application.define do
  helpers do
    def ask_oracle(queue, text)
      width = cols - 12
      queue << "you: #{text}"
      Thread.new do
        reply = ORACLE.reply(text, context)
        BBS::FrameBuffer.wrap_ansi("ORACLE: #{reply}", width).each { |line| queue << line }
        queue << ''
      rescue StandardError => e
        warn "Oracle: #{e.class}: #{e.message}"
      end
    end
  end

  window :oracle, type: :stream do |w|
    w.title       ' The Oracle '
    w.placeholder 'Ask the Oracle and press Enter…'
    w.initial_lines do |ctx|
      ctx[:oracle_queue] = Queue.new
      ['ORACLE: I am here, caller.', '']
    end
    w.on_submit { |text, app, ctx| app.ask_oracle(ctx[:oracle_queue], text.strip) }
    w.on_poll do |_app, ctx|
      lines = []
      lines << ctx[:oracle_queue].pop until ctx[:oracle_queue].empty?
      lines
    end
  end

  status_hint('F1', 'Oracle') { open_window(:oracle) }
  status_hint('Alt-X', 'Bye') { :halt }
end

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    call do |ctx, runner|
      BOARD.run(session: runner.session, session_id: ctx[:session_id], context: ctx)
      nil
    end
  end
end

BBS.start
```

A window spec `ctx`-je a window minden megnyitásakor új Hash, ezért a Queue ott él; a chat előzménye az application `context`-jébe kerül, ami a hívás végéig megmarad. Így működik a Teletype BBS TTG Master window-ja is (`ask_master` a `lib/ui/modern.rb`-ben), egy „egyszerre egy kérdés” őrrel kiegészítve.

## Tesztelés modell nélkül

A `Chat`-nek csak `enabled?` és `stream` kell a kliensétől, így egy teszt csonk objektumot is átadhat. Ha a valódi `Client`-et is ki akarod próbálni, néhány sor Ruby egy helyi helyettes végpontot ad, ami egy fix választ streamel:

```ruby
# stub_llm.rb — csak tesztekhez
require 'socket'
require 'json'

server = TCPServer.new('127.0.0.1', 2480)
loop do
  client = server.accept
  headers = {}
  while (line = client.gets) && line != "\r\n"
    name, value = line.split(':', 2)
    headers[name.downcase] = value.to_s.strip if value
  end
  body = JSON.parse(client.read(headers['content-length'].to_i))
  warn "model=#{body['model'].inspect} messages=#{body['messages'].size}"
  client.write "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nConnection: close\r\n\r\n"
  ['The **line** ', 'is quiet ', 'tonight, ', 'caller.'].each do |piece|
    client.write "data: #{JSON.generate(choices: [{ delta: { content: piece } }])}\n\n"
  end
  client.write "data: [DONE]\n\n"
  client.close
end
```

```ruby
require 'bbs'

PERSONA = BBS::AI::Persona.load(File.join(__dir__, 'oracle.yml'))
CLIENT  = BBS::AI::Client.new(url: 'http://127.0.0.1:2480', model: 'qwen3.5-0.8b', api_key: 'YOUR_KEY')
ORACLE  = BBS::AI::Chat.new(persona: PERSONA, client: CLIENT, history: 2, key: :oracle_history)

ctx = {}
puts ORACLE.reply('is anyone there?', ctx) { |piece| print "[#{piece}]" }
# [The line ][is quiet ][tonight, ][caller. ]The line is quiet tonight, caller.
puts ORACLE.reply('and now?', ctx)
p ctx[:oracle_history]
# [{:role=>"user", :content=>"and now?"}, {:role=>"assistant", :content=>"The line is quiet tonight, caller."}]
```

A `**` jelek eltűntek (a `filter`), és `history: 2` mellett csak az utolsó kör marad meg. Az első kérés hat üzenetet vitt (a system promptot, négy példaüzenetet, a kérdést), a második nyolcat (plusz a megjegyzett kört).

# Kis modellek élesben

A boardjaink egymilliárdnál kevesebb paraméteres modellen futtatják a karaktereiket (Qwen3.5 0.8B a közös llama.cpp-szolgáltatáson). Egy ilyen kis modell elég jól fogalmaz, de tényeket nem tart meg, és kiesik a szerepéből. Ami bevált, sok futáson mérve:

**A tényeket a kód tartja, a modell csak megfogalmazza őket.**

- **Chat of Ra** (`bbs/teletype-bbs`, `lib/doors/ra.rb` és `lib/characters/ra.rb`): a talányok, a válaszaik és a tippek a `ra_riddles.yml`-ben vannak; a kód teszi fel a talányt és ellenőrzi a választ, így a jó válasz mindig jó. A játékra vonatkozó kérdés (a `GAME_WORDS` szerint) a katalógusból épített sort kapja, nem a modelltől, mert a modell elkeveri a címet. Csak a szabad beszélgetés megy a modellhez.
- **A TTG Master** (`lib/characters/master.rb`): a sysop, aki a boardról szóló kérdésekre felel. Minden kérdéshez a kód legfeljebb két tényt keres a `master_knowledge.yml`-ben (kulcsszavak; a kifejezés-kulcs dupla, az olyan általános szó, mint a „board”, fél súllyal számít), és élő tényeket (legutóbbi kiadások, közelgő események, a katalógus). Tény nélküli kérdés sosem jut el a modellhez: a persona fallbackje válaszol. Különben a tények a **user-üzenetbe** kerülnek („Facts: … Question: …”), a persona példái is így néznek ki, és `history: 0`. A választ a kód utána ellenőrzi: meg kell tartania a tények legalább egy horgonyát (menüút, cím, dátum), és nem nevezhet meg olyan menüutat, ami nincs bennük. Ha elbukik, vagy a modell nem elérhető, a hívó az első tényt kapja szó szerint (`fallback: facts.first.text`). A `facts:` kulcsszó ilyen célra való; a Masternél a tények azért a user-körben vannak, mert a 0.8B-s modell a system promptban figyelmen kívül hagyta őket.
- **SERMO** (`bbs/sermo-bbs`, `lib/sermo/acp.yml` és `lib/sermo/oracle.rb`): egy karakter, akinek nincs kikeresendő ténye, csak egy világa. `history: 2`, mert hosszabb memóriával a modell a saját botlásait másolja.

**Példákkal taníts, ne utasításokkal.** Egy kis modell sokkal jobban másolja a példáit, mint ahogy a system promptot követi. Egy új tényt vagy hangnemet legjobb még egy példakörként tanítani. Azt a példakört, ami folyton átszivárog a válaszokba, inkább vedd ki.

**Amit mindig elront, azt deflectáld.** Kód, versek, „bot vagy?”: akárhogy kérdezik, egy kis modell ezekre kiesik a szerepéből. Egy `deflect`-szabály modell nélkül válaszol rájuk:

```yaml
deflect:
  - match: '\b(code|script|program(ming)?|python|javascript|java|html|sql|function)\b'
    reply: The scribes wrote, and they are dust now. I do not write; I shine.
  - match: '\b(chat ?bot|bot|robot|ai|artificial|machine|computer)\b'
    reply: Those are words from a land I have never shone on. I am Ra, and you are the one who prayed.
```

**Rövid válaszok, korai megállás.** `max_tokens` 80 és 110 körül, alacsony `temperature` (0,3–0,4), és `stop: ["\n\n", "```"]`, hogy egy botlásból sose nőjön második bekezdés vagy program. A Qwen3.5 0.8B-n az ismétlésbüntetések (`presence_penalty`, `frequency_penalty`, `repeat_penalty`) miatt a modell túlbeszélt a `max_tokens`-en; a SERMO personájából ki is kerültek.

**Mérj, ne egyes válaszokból ítélj.** A `bbs/teletype-bbs` `bin/persona-eval`-ja többször felteszi egy karakternek a `lib/characters/eval/NAME.yml` kérdéseit, és megszámolja a botlásokat: szerepen kívüli szavak, markdown vagy emoji, egy-két terminálsornál hosszabb válasz, és a Masternél a várt kifejezések hiánya. Ezekre a számokra hangold a personát:

```text
BBS_LLM_URL=http://localhost:8080 bin/persona-eval ra 3
docker exec teletype-bbs bin/persona-eval master 3
```

**Tartsd betöltve a modellt.** A kliens `read_timeout`-ja 30 s; egy modell, ami csak az első kérésre töltődik be, nem biztos, hogy időben válaszol. A szolgáltatásunk induláskor betölti a SERMO modelljét.

# Lásd még

- [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs) — az áttekintés és a tartalomjegyzék
- [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors) — a FakeOS, ahol a legtöbb karakter él, és a doorok
- [Window-k](https://wiki.teletypegames.org/hu/engines/rubbs/windows) — a `Stream` window egy asztalon belüli chathez
- [Application](https://wiki.teletypegames.org/hu/engines/rubbs/application) — helperek, context, a tétlenségi tick
- [Változásnapló](https://wiki.teletypegames.org/hu/engines/rubbs/changelog) — BBS::AI (0.5.0), `facts:`, `fallback:` és `deflect` (0.8.0)

'; var prevLevel = 2; for (var i = 0; i < headings.length; i++) { var h = headings\[i\]; var text = h.textContent.trim(); var slug = text.toLowerCase() .replace(/\[áàâä\]/g,'a').replace(/\[éèêë\]/g,'e').replace(/\[íìîï\]/g,'i') .replace(/\[óòôöő\]/g,'o').replace(/\[úùûüű\]/g,'u') .replace(/\[^a-z0-9\]+/g, '-').replace(/^-|-$/g, ''); if (!slug) slug = 'heading-' + i; if (used\[slug\]) { slug += '-' + i; } used\[slug\] = true; h.id = slug; var level = parseInt(h.tagName.charAt(1)); var cls = level > 2 ? ' class="toc-child"' : ''; html += '- [' + text + '](#'%20+%20slug%20+%20')
'; } html += '
'; toc.innerHTML = html; // Scroll-spy: highlight current heading var links = toc.querySelectorAll('a'); var ids = \[\]; for (var j = 0; j < links.length; j++) ids.push(links\[j\].getAttribute('href').slice(1)); function updateActive() { var current = ''; for (var k = 0; k < ids.length; k++) { var el = document.getElementById(ids\[k\]); if (el && el.getBoundingClientRect().top <= 80) current = ids\[k\]; } for (var m = 0; m < links.length; m++) { links\[m\].parentNode.classList.toggle('active', links\[m\].getAttribute('href') === '#' + current); } } window.addEventListener('scroll', updateActive, { passive: true }); updateActive(); })();

---

## Navigation

- Parent: [RuBBS](https://wiki.teletypegames.org/hu/engines/rubbs.md)
- Next: [Application](https://wiki.teletypegames.org/hu/engines/rubbs/application.md)
