2026-04-10 12:22:20 -04:00
# Техническая спецификация формата вывода JSONL для запросов
2026-04-04 18:04:39 -04:00
## Обзор
2026-04-10 12:22:20 -04:00
Эта спецификация описывает реализацию формата вывода JSONL (JSON Lines) для ответов на запросы в TrustGraph. JSONL обеспечивает устойчивость к усечению при извлечении структурированных данных из ответов LLM, решая критические проблемы, связанные с повреждением выходных массивов JSON, когда ответы LLM достигают лимита токенов.
2026-04-04 18:04:39 -04:00
Эта реализация поддерживает следующие сценарии использования:
2026-04-10 12:22:20 -04:00
1. **Извлечение, устойчивое к усечению** : Извлекайте допустимые частичные результаты, даже когда
вывод LLM усекается в середине ответа.
2. **Извлечение в больших масштабах** : Обрабатывайте извлечение большого количества элементов без риска
полной сбоя из-за ограничений на количество токенов.
3. **Извлечение данных разных типов** : Поддерживайте извлечение нескольких типов сущностей
(определения, отношения, сущности, атрибуты) в одном запросе.
4. **Вывод, совместимый с о стримингом** : Обеспечьте возможность будущей потоковой/инкрементной
обработки результатов извлечения.
2026-04-04 18:04:39 -04:00
## Цели
2026-04-10 12:22:20 -04:00
**Обратная совместимость**: Существующие запросы, использующие `response-type: "text"` и
`response-type: "json"` , продолжают работать без изменений.
**Устойчивость к усечению**: Частичные выходные данные LLM приводят к частичным, но допустимым результатам,
а не к полному сбою.
**Проверка схемы**: Поддержка проверки JSON-схемы для отдельных объектов.
**Дискриминированные объединения**: Поддержка выходных данных смешанных типов с использованием поля `type`
в качестве дискриминатора.
**Минимальные изменения API**: Расширение существующей конфигурации запросов с добавлением нового
типа ответа и ключа схемы.
## Обзор
### Текущая архитектура
Сервис запросов поддерживает два типа ответов:
1. `response-type: "text"` - Необработанный текстовый ответ, возвращаемый как есть.
2. `response-type: "json"` - JSON, полученный из ответа и проверенный на соответствие
необязательной `schema` .
Текущая реализация в `trustgraph-flow/trustgraph/template/prompt_manager.py` :
```python
class Prompt:
def __init__ (self, template, response_type = "text", terms=None, schema=None):
self.template = template
self.response_type = response_type
self.terms = terms
self.schema = schema
```
### Текущие ограничения
Когда запросы извлечения требуют вывод в виде массивов JSON (`[{...}, {...}, ...]` ):
**Повреждение из-за усечения**: Если языковая модель достигает лимита выходных токенов в середине массива,
весь ответ становится недействительным JSON и не может быть проанализирован.
**Анализ "все или ничего"**: Необходимо получить полный вывод перед анализом.
**Нет частичных результатов**: Усеченный ответ дает нулевые полезные данные.
**Ненадежно для больших извлечений**: Чем больше извлеченных элементов, тем выше риск сбоя.
Данная спецификация решает эти ограничения, вводя формат JSONL для
запросов извлечения, где каждый извлеченный элемент является полным JSON-объектом на своей
строке.
## Техническое проектирование
### Расширение типа ответа
Добавьте новый тип ответа `"jsonl"` наряду с существующими типами `"text"` и `"json"` .
#### Изменения конфигурации
**Новое значение типа ответа:**
```
"response-type": "jsonl"
```
**Интерпретация схемы:**
Существующий ключ `"schema"` используется как для `"json"` , так и для `"jsonl"` типов ответов.
Интерпретация зависит от типа ответа:
`"json"` : Схема описывает весь ответ (обычно массив или объект).
`"jsonl"` : Схема описывает каждую отдельную строку/объект.
```json
{
"response-type": "jsonl",
"schema": {
"type": "object",
"properties": {
"entity": { "type": "string" },
"definition": { "type": "string" }
},
"required": ["entity", "definition"]
}
}
```
Это позволяет избежать изменений в инструментах и редакторах конфигурации подсказок.
### Спецификация формата JSONL
#### Простое извлечение
Для подсказок, извлекающих один тип объекта (определения, отношения,
темы, строки), вывод представляет собой один объект JSON на строку без обертки:
**Формат вывода подсказки:**
```
{"entity": "photosynthesis", "definition": "Process by which plants convert sunlight"}
{"entity": "chlorophyll", "definition": "Green pigment in plants"}
{"entity": "mitochondria", "definition": "Powerhouse of the cell"}
```
**Контраст с предыдущим форматом JSON-массива:**
```json
[
{"entity": "photosynthesis", "definition": "Process by which plants convert sunlight"},
{"entity": "chlorophyll", "definition": "Green pigment in plants"},
{"entity": "mitochondria", "definition": "Powerhouse of the cell"}
]
```
Если LLM обрезает текст после строки 2, формат массива JSON приводит к недействительному JSON,
в то время как JSONL дает два допустимых объекта.
#### Извлечение данных смешанных типов (дискриминированные объединения)
Для запросов, извлекающих несколько типов объектов (например, определения и
отношения, или сущности, отношения и атрибуты), используйте поле `"type"`
в качестве дискриминатора:
**Формат вывода запроса:**
```
{"type": "definition", "entity": "DNA", "definition": "Molecule carrying genetic instructions"}
{"type": "relationship", "subject": "DNA", "predicate": "located_in", "object": "cell nucleus", "object-entity": true}
{"type": "definition", "entity": "RNA", "definition": "Molecule that carries genetic information"}
{"type": "relationship", "subject": "RNA", "predicate": "transcribed_from", "object": "DNA", "object-entity": true}
```
**Схема для дискриминированных объединений использует `oneOf` :**
```json
{
"response-type": "jsonl",
"schema": {
"oneOf": [
{
2026-04-04 18:04:39 -04:00
"type": "object",
"properties": {
2026-04-10 12:22:20 -04:00
"type": { "const": "definition" },
"entity": { "type": "string" },
"definition": { "type": "string" }
2026-04-04 18:04:39 -04:00
},
2026-04-10 12:22:20 -04:00
"required": ["type", "entity", "definition"]
},
{
"type": "object",
"properties": {
"type": { "const": "relationship" },
"subject": { "type": "string" },
"predicate": { "type": "string" },
"object": { "type": "string" },
"object-entity": { "type": "boolean" }
},
"required": ["type", "subject", "predicate", "object", "object-entity"]
2026-04-04 18:04:39 -04:00
}
2026-04-10 12:22:20 -04:00
]
}
}
```
#### Извлечение онтологии
Для извлечения онтологии на основе сущностей, связей и атрибутов:
**Формат вывода запроса:**
```
{"type": "entity", "entity": "Cornish pasty", "entity_type": "fo/Recipe"}
{"type": "entity", "entity": "beef", "entity_type": "fo/Food"}
{"type": "relationship", "subject": "Cornish pasty", "subject_type": "fo/Recipe", "relation": "fo/has_ingredient", "object": "beef", "object_type": "fo/Food"}
{"type": "attribute", "entity": "Cornish pasty", "entity_type": "fo/Recipe", "attribute": "fo/serves", "value": "4 people"}
```
### Детали реализации
#### Класс Prompt
Существующий класс `Prompt` не требует изменений. Поле `schema` используется повторно
для JSONL, а е г о интерпретация определяется `response_type` :
```python
class Prompt:
def __init__ (self, template, response_type="text", terms=None, schema=None):
self.template = template
self.response_type = response_type
self.terms = terms
self.schema = schema # Interpretation depends on response_type
```
#### PromptManager.load_config
Изменения не требуются - существующая загрузка конфигурации уже обрабатывает
ключ `schema` .
#### Разбор JSONL
Добавлен новый метод разбора ответов в формате JSONL:
```python
def parse_jsonl(self, text):
"""
Parse JSONL response, returning list of valid objects.
Invalid lines (malformed JSON, empty lines) are skipped with warnings.
This provides truncation resilience - partial output yields partial results.
"""
results = []
for line_num, line in enumerate(text.strip().split('\n'), 1):
line = line.strip()
# Skip empty lines
if not line:
continue
# Skip markdown code fence markers if present
if line.startswith('```'):
continue
try:
obj = json.loads(line)
results.append(obj)
except json.JSONDecodeError as e:
# Log warning but continue - this provides truncation resilience
logger.warning(f"JSONL parse error on line {line_num}: {e}")
return results
```
#### Изменения в PromptManager.invoke
Расширить метод invoke для обработки нового типа ответа:
```python
async def invoke(self, id, input, llm):
logger.debug("Invoking prompt template...")
terms = self.terms | self.prompts[id].terms | input
resp_type = self.prompts[id].response_type
prompt = {
"system": self.system_template.render(terms),
"prompt": self.render(id, input)
2026-04-04 18:04:39 -04:00
}
2026-04-10 12:22:20 -04:00
resp = await llm(**prompt)
if resp_type == "text":
return resp
if resp_type == "json":
try:
obj = self.parse_json(resp)
except:
logger.error(f"JSON parse failed: {resp}")
raise RuntimeError("JSON parse fail")
if self.prompts[id].schema:
try:
validate(instance=obj, schema=self.prompts[id].schema)
logger.debug("Schema validation successful")
except Exception as e:
raise RuntimeError(f"Schema validation fail: {e}")
return obj
if resp_type == "jsonl":
objects = self.parse_jsonl(resp)
if not objects:
logger.warning("JSONL parse returned no valid objects")
return []
# Validate each object against schema if provided
if self.prompts[id].schema:
validated = []
for i, obj in enumerate(objects):
try:
validate(instance=obj, schema=self.prompts[id].schema)
validated.append(obj)
except Exception as e:
logger.warning(f"Object {i} failed schema validation: {e}")
return validated
return objects
raise RuntimeError(f"Response type {resp_type} not known")
```
### Затронутые запросы
Следующие запросы должны быть перенесены в формат JSONL:
| Идентификатор запроса | Описание | Тип поля |
|-----------|-------------|------------|
| `extract-definitions` | Извлечение сущностей/определений | Нет (один тип) |
| `extract-relationships` | Извлечение отношений | Нет (один тип) |
| `extract-topics` | Извлечение темы/определения | Нет (один тип) |
2026-04-04 18:04:39 -04:00
| `extract-rows` | Извлечение структурированных строк | Нет (один тип) |
2026-04-10 12:22:20 -04:00
| `agent-kg-extract` | Комбинированное извлечение определений + отношений | Да: `"definition"` , `"relationship"` |
| `extract-with-ontologies` / `ontology-extract` | Извлечение на основе онтологии | Да: `"entity"` , `"relationship"` , `"attribute"` |
### Изменения API
#### Точка зрения клиента
Разбор JSONL прозрачен для вызывающих API сервиса запросов. Разбор происходит
на стороне сервера в сервисе запросов, и ответ возвращается через стандартное
поле `PromptResponse.object` в виде сериализованного массива JSON.
Когда клиенты вызывают сервис запросов (через `PromptClient.prompt()` или аналогично):
**`response-type: "json"` ** с схемой массива → клиент получает Python `list`
**`response-type: "jsonl"` ** → клиент получает Python `list`
С точки зрения клиента, о б а возвращают идентичные структуры данных. Разница заключается только в том, как вывод LLM разбирается на стороне сервера:
2026-04-04 18:04:39 -04:00
2026-04-10 12:22:20 -04:00
Формат массива JSON: Один вызов `json.loads()` ; полностью завершается с ошибкой, если усечен
Формат JSONL: Разбор построчно; дает частичные результаты, если усечен
2026-04-04 18:04:39 -04:00
2026-04-10 12:22:20 -04:00
Это означает, что существующий клиентский код, ожидающий список от запросов извлечения,
не требует изменений при переносе запросов из формата JSON в формат JSONL.
2026-04-04 18:04:39 -04:00
2026-04-10 12:22:20 -04:00
#### Возвращаемое значение сервера
Для `response-type: "jsonl"` метод `PromptManager.invoke()` возвращает
`list[dict]` , содержащий все успешно разобранные и проверенные объекты. Этот
список затем сериализуется в JSON для поля `PromptResponse.object` .
### Обработка ошибок
Пустые результаты: Возвращает пустой список `[]` с предупреждением в журнале
Частная ошибка разбора: Возвращает список успешно разобранных объектов с
предупреждениями в журналах о б ошибках
Полная ошибка разбора: Возвращает пустой список `[]` с предупреждениями в журналах
Это отличается от `response-type: "json"` , который вызывает `RuntimeError` при
ошибке разбора. Легкое поведение для JSONL намеренно, чтобы обеспечить
устойчивость к усечению.
### Пример конфигурации
Полный пример конфигурации запроса:
```json
{
"prompt": "Extract all entities and their definitions from the following text. Output one JSON object per line.\n\nText:\n{{text}}\n\nOutput format per line:\n{\"entity\": \"< name > \", \"definition\": \"< definition > \"}",
"response-type": "jsonl",
"schema": {
"type": "object",
"properties": {
"entity": {
"type": "string",
"description": "The entity name"
},
"definition": {
"type": "string",
"description": "A clear definition of the entity"
}
},
"required": ["entity", "definition"]
}
}
```
## Соображения безопасности
**Проверка входных данных**: Разбор JSON использует стандартную `json.loads()` , что безопасно
от атак внедрения.
**Проверка схемы**: Используется `jsonschema.validate()` для обеспечения соответствия схеме.
**Отсутствие новых уязвимостей**: Разбор JSONL значительно безопаснее, чем разбор JSON-массивов,
благодаря обработке построчно.
## Соображения производительности
**Память**: Построчный разбор использует меньше пиковой памяти, чем загрузка полных
JSON-массивов.
**Задержка**: Производительность разбора сопоставима с разбором JSON-массивов.
**Проверка**: Проверка схемы выполняется для каждого объекта, что добавляет накладные
расходы, но позволяет получать частичные результаты в случае сбоя проверки.
2026-04-04 18:04:39 -04:00
## Стратегия тестирования
2026-04-10 12:22:20 -04:00
### Юнит-тесты
Разбор JSONL с допустимыми входными данными.
Разбор JSONL с пустыми строками.
Разбор JSONL с блоками форматированного текста Markdown.
Разбор JSONL с обрезанной последней строкой.
Разбор JSONL с о строками, содержащими недопустимый JSON.
Проверка схемы с использованием `oneOf` для дискриминируемых объединений.
Обратная совместимость: существующие `"text"` и `"json"` подсказки не изменены.
### Интеграционные тесты
Комплексная извлечение данных с использованием подсказок JSONL.
Извлечение данных с имитацией усечения (искусственно ограниченный ответ).
Извлечение данных смешанных типов с использованием дискриминатора типов.
Извлечение данных онтологии с о всеми тремя типами.
### Тесты качества извлечения данных.
Сравнение результатов извлечения: формат JSONL против массива JSON.
Проверка устойчивости к усечению: JSONL возвращает частичные результаты, в то время как JSON - нет.
## План миграции
### Этап 1: Реализация
1. Реализовать метод `parse_jsonl()` в `PromptManager` .
2. Расширить `invoke()` для обработки `response-type: "jsonl"` .
3. Добавить модульные тесты.
### Этап 2: Миграция подсказок
1. Обновить подсказку `extract-definitions` и конфигурацию.
2. Обновить подсказку `extract-relationships` и конфигурацию.
3. Обновить подсказку `extract-topics` и конфигурацию.
4. Обновить подсказку `extract-rows` и конфигурацию.
5. Обновить подсказку `agent-kg-extract` и конфигурацию.
6. Обновить подсказку `extract-with-ontologies` и конфигурацию.
### Этап 3: Обновления для последующих этапов
1. Обновить любой код, использующий результаты извлечения, чтобы он мог обрабатывать возвращаемый тип списка.
2. Обновить код, который категоризует извлечения смешанных типов, используя поле `type` .
3. Обновить тесты, которые проверяют формат выходных данных извлечения.
## Открытые вопросы
Н а данный момент нет.
2026-04-04 18:04:39 -04:00
## Ссылки
2026-04-10 12:22:20 -04:00
Текущая реализация: `trustgraph-flow/trustgraph/template/prompt_manager.py`
Спецификация JSON Lines: https://jsonlines.org/
Схема JSON `oneOf` : https://json-schema.org/understanding-json-schema/reference/combining.html#oneof
Связанная спецификация: Streaming LLM Responses (`docs/tech-specs/streaming-llm-responses.md` )