Головна / Статті / Практичні нотатки: Як працює MCP: детальний аналіз із кодом

Практичні нотатки: Як працює MCP: детальний аналіз із кодом

Покроковий посібник з практичних нотаток: як працює MCP: детальний аналіз із кодом – контракти, перевірки та слоти для вставки коду для команд, які використовують цю схему.

3070 слів

У цьому посібнику детально описано шлях від сировини до функціональної системи для матеріалу «Як працює MCP: глибокий аналіз із кодом». Основна увага приділяється конкретним крокам виконання, чітким перевіркам та коду, який можна просто додати до репозиторію, не намагаючись здогадатися про його призначення. На етапі огляду необхідно визначити вхідні дані, виконавця кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись зрозуміти прихований стан системи. Поруч із функціональними результатами слід записувати час виконання та витрати на токени або запити. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-версії до спільних середовищ.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "search_web",
        "description": "Search the web for a given query",
        "inputSchema": {
          "type": "object",
          "properties": {
            "query": { "type": "string" }
          },
          "required": ["query"]
        }
      }
    ]
  }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "search_web",
    "arguments": {
      "query": "latest news on MCP protocol"
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Anthropic released MCP in Nov 2024 as an open standard..."
      }
    ]
  }
}

Що таке FastMCP?

Під час вивчення етапу «Що таке FastMCP» спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Зберігайте конфігурацію окремо від коду додатку. Файли середовища, сховища секретів та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Фіксуйте назву інструменту, хеш аргументів, час відгуку та результат кожного виклику. Без цих записів налагодження агента займає години.

from fastmcp import FastMCP

mcp = FastMCP("My Server")   # creates the server

@mcp.tool()                  # registers the function as an MCP tool
def add(a: float, b: float) -> float:
    """Add two numbers."""    # docstring → tool description sent to the LLM
    return a + b             # type hints → JSON Schema sent to the LLM

mcp.run()                    # starts the stdio message loop

Три примітиви MCP

Під час роботи над етапом «Три примітиви MCP» спочатку запишіть умови використання: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Одночасно задокументуйте шлях успішного виконання та шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Фіксуйте назву інструменту, хеш аргументів, час відгуку та результат кожного виклику. Без цих записів дебагування може займати години.

# servers/math_server.py

@mcp.tool()
def divide(a: float, b: float) -> float:
    """Divide a by b. Raises an error if b is zero."""
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# servers/math_server.py

@mcp.resource("math://constants")
def get_math_constants() -> str:
    """Common mathematical constants."""
    return f"π = {math.pi}\n e = {math.e}\n ..."

@mcp.resource("math://formulas/{category}")
def get_formulas(category: str) -> str:
    """Retrieve mathematical formulas by category (geometry | algebra | statistics)."""
    catalog = {
        "geometry": (
            "Geometry Formulas:\n"
            "  Circle area:            A = π × r²\n"
            "  Circle circumference:   C = 2π × r\n"
            "  Rectangle area:         A = length × width\n"
            "  Triangle area:          A = (base × height) / 2\n"
            "  Sphere volume:          V = (4/3) × π × r³\n"
        ),
        "algebra": (
            "Algebra Formulas:\n"
            "  Quadratic formula:      x = (−b ± √(b²−4ac)) / 2a\n"
            "  Difference of squares:  a²−b² = (a+b)(a−b)\n"
            "  Perfect square:         (a+b)² = a²+2ab+b²\n"
            "  Sum of arithmetic seq:  S = n(a₁+aₙ)/2\n"
        ),
        "statistics": (
            "Statistics Formulas:\n"
            "  Mean:       μ = Σx / n\n"
            "  Variance:   σ² = Σ(x−μ)² / n\n"
            "  Std Dev:    σ = √(Σ(x−μ)² / n)\n"
            "  Z-score:    z = (x−μ) / σ\n"
        ),
    }
    return catalog.get(
        category,
        f"Unknown category '{category}'. Available: geometry, algebra, statistics",
    )
# servers/math_server.py

@mcp.prompt()
def math_tutor(difficulty: str = "intermediate") -> str:
    return (
        f"You are an expert math tutor for {difficulty}-level students. "
        "Break every problem into numbered steps..."
    )

Підсумок сервера

Під час роботи над етапом підсумку сервера спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Віддавайте перевагу невеликим, тестованим одиницям коду перед об’ємними скриптами. Якщо якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність операцій. Фіксуйте назву інструменту, хеш аргументів, час відгуку та результат кожного виклику. Без цих даних пошук помилок займає години.

З точки зору клієнта

Під час роботи на етапі «Від клієнта» спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте беззвучного часткового виконання завдань. Фіксуйте назву інструменту, хеш аргументів, час виконання та результат кожного виклику. Без цих записів дебагування займає години.

You type a query
      │
      ▼
main.py                    ← entry point, parses args, kicks off async loop
      │
      ▼
client/agent.py            ← spawns 3 MCP servers, builds the agent, invokes it
      │           │
      │           ▼
      │    utils/tracker.py ← fires on every LLM call, tool call, and result
      │
      ▼
LangGraph ReAct loop       ← think → call tool → observe → repeat
      │
      ▼
servers/{math,text,data}_server.py   ← each runs as an isolated subprocess

Крок 1 — точка входу

Під час виконання першого кроку — етапу планування, спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Запишіть час виконання та витрати на токени або запити поруч із функціональними результатами. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам, коли процес переходить від демо-середовища до спільних. Записуйте назву інструменту, хеш аргументів, час відгуку та результат кожного виклику. Без цих записів пошук помилок у агенті займає години.

async def _run(queries: list) -> None:
    from client.agent import run_query   # imported here (late) to keep startup fast
    for i, q in enumerate(queries):
        await run_query(q)

def main() -> None:
    ...
    asyncio.run(_run(queries))
Generate 8 random numbers between 5 and 50 using seed=42,
then calculate their statistics, and tell me if there are any outliers.
python3 main.py --query "Generate 8 random numbers between 5 and 50 using seed=42, \
then calculate their statistics, and tell me if there are any outliers."
╭────────────────────────────── 🔌  MCP Example ───────────────────────────────╮
│ Multi-Server MCP Demo                                                        │
│                                                                              │
│ Three FastMCP servers, each exposing tools + resources + prompts:            │
│   ●  Math Server   — add, subtract, multiply, divide, power, sqrt,           │
│ percentage                                                                   │
│   ●  Text Server   — count_words, word_frequency, reverse, transform,        │
│ extract_emails                                                               │
│   ●  Data Server   — generate_numbers, calculate_statistics, find_outliers,  │
│ normalise                                                                    │
│                                                                              │
│ Stack : FastMCP · LangChain · LangGraph · OpenAI · Rich                      │
│ Track : live Rich panels + JSONL log files under logs/                       │
╰──────────────────────────────────────────────────────────────────────────────╯

Крок 2 — Створення агента

Під час виконання кроку 2 «Створення сцени» спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища секретних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Фіксуйте назву інструменту, хеш аргументів, час відгуку та результат кожного виклику. Без цих записів дебагування агента займає години.

log_file = str(LOGS_DIR / f"run_{int(time.time())}.jsonl")
tracker = MCPTracker(log_file=log_file)
╭──── 💬  USER QUERY ────╮
│ Generate 8 random...   │
╰────────────────────────╯
def _server_config() -> Dict[str, Any]:
    silent_env = {**os.environ, "FASTMCP_LOG_LEVEL": "ERROR"}
    return {
        "math_server": {
            "command": sys.executable,
            "args": ["servers/math_server.py"],
            "transport": "stdio",
            "env": silent_env,
        },
        "text_server": { ... },
        "data_server":  { ... },
    }
client = MultiServerMCPClient(_server_config())
tools = await client.get_tools()
Connected to 3 servers (math_server, text_server, data_server) with 19 tools: add, subtract, multiply,
divide, power, square_root, calculate_percentage, count_words, word_frequency, reverse_text,
transform_case, find_and_replace, extract_emails, count_vowels_consonants, generate_numbers,
calculate_statistics, find_outliers, sort_values, normalize_values
model = ChatOpenAI(model=model_name, temperature=0)
agent = create_react_agent(model, tools)
START
  │
  ▼
[call_model]  ─── no tool call ──▶  END
      │
  tool call requested
      │
      ▼
[call_tools]
      │
      ▼
[call_model]  (loop again with tool result in context)
config = {"callbacks": [tracker]}
result = await agent.ainvoke({"messages": [("human", query)]}, config=config)

Крок 3 — Спостереження за всім у реальному часі

Під час виконання етапу 3 «Спостереження за всім», спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Одночасно задокументуйте шлях успішного виконання та шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Фіксуйте назву інструменту, хеш аргументів, час відгуку та результат кожного виклику. Без цих записів дебагування агента займає години.

agent.ainvoke() called
      │
      ├─▶ on_chain_start()       "▶ AGENT STARTED" panel
      │
      ├─▶ on_chat_model_start()  "🤖 LLM CALL #1" panel  (timer starts)
      ├─▶ on_llm_end()           "LLM responded ⏱ 1.23s → will call: add, multiply"
      │
      ├─▶ on_tool_start()        "🔧 TOOL CALL #1" panel  (timer starts)
      ├─▶ on_tool_end()          "✓ TOOL RESULT ⏱ 0.01s" panel
      │
      ├─▶ on_tool_start()        (second tool, if any)
      ├─▶ on_tool_end()
      │
      ├─▶ on_chat_model_start()  "🤖 LLM CALL #2" (LLM synthesises final answer)
      ├─▶ on_llm_end()           no tools this time → loop ends
      │
      └─▶ agent.ainvoke() returns
TOOL_SERVER_MAP = {
    "add":        "math_server",
    "count_words": "text_server",
    "generate_numbers": "data_server",
    ...
}
SERVER_COLORS = {
    "math_server": "cyan",
    "text_server":  "green",
    "data_server":  "yellow",
}
── 🤖  LLM CALL  #1  model=gpt-5-mini   messages=1 ──╮
│  Role    Content preview                             │
│  Human   Generate 8 random numbers between 5 and 50… │
╰──────────────────────────────────────────────────────╯
  LLM responded  ⏱ 5.35s  → will call: generate_numbers
{
  "name": "generate_numbers",
  "arguments": {"count": 8, "min_val": 5, "max_val": 50, "seed": 42}
}
╭── 🔧  TOOL CALL  #1 ──────────────────╮
│ Tool  :  generate_numbers             │
│ Server:  data_server                  │
│ Args  :                               │
│ {'count': 8, 'min_val': 5,            │
│  'max_val': 50, 'seed': 42}           │
╰───────────────────────────────────────╯
{"method": "tools/call", "params": {"name": "generate_numbers",
  "arguments": {"count": 8, "min_val": 5, "max_val": 50, "seed": 42}}}
╭── ✓  TOOL RESULT  ⏱ 0.63s ────────────────────────────╮
│ [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91] │
╰──────────────────────────────────────────────────────────╯
random.seed(42)
return [round(random.uniform(5, 50), 2) for _ in range(8)]
# → [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91]
╭── 🤖  LLM CALL  #2  model=gpt-5-mini   messages=3 ──╮
│  Role    Content preview                             │
│  Human   Generate 8 random numbers…                 │
│  AI      (the tool-call decision)                    │
│  Tool    [33.77, 6.13, 17.38, ...]                   │
╰──────────────────────────────────────────────────────╯
Tool  : calculate_statistics
Server: data_server
Args  : {'numbers': [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91]}
Result: {"count":8, "min":6.13, "max":45.15, "mean":24.9962,
         "median":25.575, "std_dev":14.8181, "variance":219.5755,
         "range":39.02, "q1":15.04, "q3":38.14}
Tool  : find_outliers
Server: data_server
Args  : {'numbers': [...], 'threshold': 2}
Result: {"outliers": [], "outlier_count": 0,
         "total_checked": 8, "mean": 24.9962, "std_dev": 14.8181}
╭── 🤖  LLM CALL  #4  messages=7 ──╮
│  Human / AI / Tool                │
│  AI    / Tool                     │
│  AI    / Tool                     │
╰───────────────────────────────────╯
  LLM responded  ⏱ 30.25s
╭── ✅  FINAL ANSWER ───────────────────────────────╮
│ Random numbers: [33.77, 6.13, 17.38, ...]        │
│ Mean: 24.9962 / Median: 25.575 / Std Dev: 14.8181│
│ Outliers: none (all |z| < 2)                     │
╰───────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────╮
│ LLM Calls   4                                       │
│ Tool Calls  3                                       │
│ Total Time  42.25s                                  │
│ Log File    logs/run_1776864980.jsonl               │
╰─────────────────────────────────────────────────────╯

Повна послідовність повідомлень

Під час роботи над етапом «Повна послідовність повідомлень» спочатку запишіть умови використання: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Віддавайте перевагу невеликим, тестованим одиницям коду перед об’ємними скриптами. Якщо якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на складну послідовність операцій. Фіксуйте назву інструменту, хеш аргументів, час виконання та результат кожного виклику. Без цих даних пошук помилок займає години. Під час роботи над етапом «Повна послідовність повідомлень» спочатку запишіть умови використання: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам, коли процес переходить від демо-версії до спільних середовищ.

1  HumanMessage   "Generate 8 random numbers…"
2  AIMessage      [tool_call: generate_numbers({count:8, min:5, max:50, seed:42})]
3  ToolMessage    [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91]
4  AIMessage      [tool_call: calculate_statistics({numbers:[...]})]
5  ToolMessage    {count:8, mean:24.9962, std_dev:14.8181, ...}
6  AIMessage      [tool_call: find_outliers({numbers:[...], threshold:2})]
7  ToolMessage    {outliers:[], outlier_count:0, ...}
8  AIMessage      "Here are the results. Random numbers: …"   ← final answer

Журнал JSONL

Етап обробки журналу JSONL працює найкраще, коли його розглядають як вимірювану поверхню. Зберігайте один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та прапорці функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь граф. Запроваджуйте інструменти з вузькими схемами та чіткими позначеннями побічних ефектів. Хостам потрібно знати, які виклики змінюють стан, перш ніж вони автоматично схвалюють їх.

{"timestamp": "...", "event": "agent_start",  "data": {"runnable": "LangGraph", "run_id": "..."}}
{"timestamp": "...", "event": "llm_call",     "data": {"call_number": 1, "model": "gpt-5-mini", "messages": 1}}
{"timestamp": "...", "event": "llm_end",      "data": {"elapsed": "5.35s", "tool_calls_requested": ["generate_numbers"]}}
{"timestamp": "...", "event": "tool_start",   "data": {"call_number": 1, "tool": "generate_numbers", "server": "data_server", "args": "..."}}
{"timestamp": "...", "event": "tool_end",     "data": {"elapsed": "0.63s", "output_preview": "[33.77, 6.13, ...]"}}
{"timestamp": "...", "event": "llm_call",     "data": {"call_number": 2, "model": "gpt-5-mini", "messages": 3}}
...
{"timestamp": "...", "event": "run_summary",  "data": {"llm_calls": 4, "tool_calls": 3, "total_elapsed": "42.25s"}}

Джерела

Етап „Джерела“ працює найкраще, коли його розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Документуйте як успішний, так і відновлювальний сценарії. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Зробіть інструменти з вузькими схемами та чіткими позначеннями побічних ефектів доступними. Хостам потрібно знати, які виклики змінюють стан, перш ніж вони автоматично схвалюють їх.

Повідомлення від нашого засновника

Повідомлення типу The A з нашої стадії працює найкраще, коли його розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Віддавайте перевагу невеликим, тестованим одиницям перед об’ємними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на складну послідовність дій. Використовуйте інструменти з вузькими схемами та чіткими позначеннями побічних ефектів. Адміністраторам потрібно знати, які виклики змінюють стан системи, перш ніж вони автоматично схвалять їх. Повідомлення типу The A з нашої стадії працює найкраще, коли його розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-версії до спільних середовищ.

Чек-лист операцій

Етап перевірки операційних процедур працює найкраще, якщо його розглядати як вимірювану площину. Збережіть один ідеальний зразок виконання, один випадок збою та запис про скасування дій перед розширенням обсягу роботи.

Розглядайте цей етап як угоду між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте мовчазного часткового виконання завдань.

Використовуйте інструменти з вузькими схемами та чіткими позначеннями побічних ефектів. Адміністраторам потрібно знати, які виклики змінюють стан системи, перш ніж вони автоматично схвалять їх.

Коли дозволяє бюджет, додайте тест на базову функціональність, який перевіряє критичний шлях у процесі інтеграції за допомогою фікстур, а не реальних платних API.

Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-версії до спільних середовищ.

Відображайте інструменти з вузькими схемами та чіткими позначками побічних ефектів. Хостам потрібно знати, які виклики змінюють стан, перш ніж вони автоматично схвалюють їх.

Перед підвищенням версії стека заморозьте версії, збережіть ідеальний запис для критичного шляху та підтвердьте кроки відкату. У спільних середовищах необхідні обмеження швидкості, перевірки прав власності та чіткий власник для заміни секретів. Віддавайте перевагу надійності перед кмітливими одноразовими демонстраціями.

Примітка для пакету c7efc4f69698: не включайте ключі постачальника до репозиторію, встановіть ліміт токенів на сеанс та зберігайте записи поруч із фіксами для оцінки, щоб подальша заміна моделей залишалася порівнянною.

Для етапу 0 заходів з посилення безпеки необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не здогадуючись про прихований стан. Необхідно документувати як шлях успішного виконання, так і шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації.

Деталь посилення безпеки 0/888: вимірюйте час виконання, клас помилки та кількість витрачених токенів для цього заходу, а потім вирішуйте, чи зберегти зміни, ґрунтуючись на фіксованому наборі питань, а не на індивідуальних спостереженнях.

Під час виконання першого етапу заходів з посилення безпеки спочатку запишіть умови договору: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Розглядайте цей етап як договір між вхідними даними та перевіреними результатами. Придумайте назви елементів, визначте критерії успіху та не допускайте беззвучного часткового виконання завдань.

Деталь посилення безпеки 1/888: вимірюйте час виконання, клас помилки та кількість витрачених ресурсів для цього етапу, а потім вирішуйте, чи залишити зміни, ґрунтуючись на фіксованому наборі критеріїв, а не на індивідуальних спостереженнях.

Етап 2 заходів з посилення безпеки працює найкраще, коли його розглядають як вимірювану поверхню. Збережіть один ідеальний зразок роботи, один випадок невдачі та запис про скасування змін перед розширенням обсягу роботи. Тримайте конфігурацію окремо від коду програми. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код.

Деталь посилення безпеки 2/888: виміряйте час виконання, клас помилки та кількість витрачених токенів для цього запису, а потім вирішіть, чи залишити зміни, ґрунтуючись на фіксованому наборі питань, а не на окремих випадках.

Для 3-го етапу запису про посилення безпеки визначте вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не здогадуючись про прихований стан. Віддавайте перевагу невеликим, тестованим одиницям перед складними скриптами. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність дій.

Деталь посилення безпеки 3/888: виміряйте час виконання, клас помилки та кількість витрачених токенів для цього запису, а потім вирішіть, чи залишити зміни, ґрунтуючись на фіксованому наборі питань, а не на окремих випадках.