Skip to content

Commit 5c84cb5

Browse files
committed
refactor: update max_tokens handling and enhance structured JSON generation
- Modified LLMControlService to dynamically set max_tokens based on input or default value. - Introduced StructuredJSONGenerateResult class to encapsulate results and error handling in structured JSON generation. - Updated TensionScoringService to utilize the new structured JSON generation method, improving error reporting and handling. - Increased max_tokens limit across various configurations and services to accommodate larger outputs. - Added unit tests for new functionalities and ensured existing tests validate the updated behavior.
1 parent 1963062 commit 5c84cb5

15 files changed

Lines changed: 147 additions & 41 deletions

File tree

application/ai/llm_control_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def _row_to_profile(self, row: dict) -> LLMProfile:
150150
api_key=row['api_key'] or '',
151151
model=row['model'] or '',
152152
temperature=row['temperature'],
153-
max_tokens=DEFAULT_MAX_OUTPUT_TOKENS,
153+
max_tokens=int(row.get('max_tokens') or DEFAULT_MAX_OUTPUT_TOKENS),
154154
timeout_seconds=row['timeout_seconds'],
155155
extra_headers=json.loads(row.get('extra_headers') or '{}'),
156156
extra_query=json.loads(row.get('extra_query') or '{}'),

application/ai/structured_json_pipeline.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import asyncio
88
import json
99
import logging
10-
from typing import List, Optional, Tuple, Type, TypeVar
10+
from dataclasses import dataclass
11+
from typing import Generic, List, Optional, Tuple, Type, TypeVar
1112

1213
from json_repair import repair_json
1314
from pydantic import BaseModel, ValidationError
@@ -27,6 +28,17 @@
2728

2829
T = TypeVar("T", bound=BaseModel)
2930

31+
32+
@dataclass(frozen=True)
33+
class StructuredJSONGenerateResult(Generic[T]):
34+
payload: Optional[T]
35+
failure_stage: str = ""
36+
errors: Tuple[str, ...] = ()
37+
38+
@property
39+
def ok(self) -> bool:
40+
return self.payload is not None
41+
3042
# 校验或解析失败后的额外重试轮数;总次数 = 1 + 该值,且不超过全局上限。
3143
DEFAULT_MAX_RETRIES = LLM_MAX_TOTAL_ATTEMPTS - 1
3244

@@ -118,6 +130,25 @@ async def structured_json_generate(
118130
max_retries: int = DEFAULT_MAX_RETRIES,
119131
) -> Optional[T]:
120132
"""调用 LLM 获取结构化 JSON 输出,并经过清洗、修复、校验管线。"""
133+
result = await structured_json_generate_with_status(
134+
llm=llm,
135+
prompt=prompt,
136+
config=config,
137+
schema_model=schema_model,
138+
max_retries=max_retries,
139+
)
140+
return result.payload
141+
142+
143+
async def structured_json_generate_with_status(
144+
llm: LLMService,
145+
prompt: Prompt,
146+
config: GenerationConfig,
147+
schema_model: Type[T],
148+
*,
149+
max_retries: int = DEFAULT_MAX_RETRIES,
150+
) -> StructuredJSONGenerateResult[T]:
151+
"""Like structured_json_generate, but preserves failure category."""
121152
current_prompt = prompt
122153
last_errors: List[str] = []
123154
total_attempts = min(1 + max(0, max_retries), LLM_MAX_TOTAL_ATTEMPTS)
@@ -151,7 +182,11 @@ async def structured_json_generate(
151182
)
152183
await asyncio.sleep(delay)
153184
continue
154-
return None
185+
return StructuredJSONGenerateResult(
186+
payload=None,
187+
failure_stage="llm_generate",
188+
errors=tuple(last_errors),
189+
)
155190

156191
cleaned = sanitize_llm_output(raw)
157192
data, parse_errors = parse_and_repair_json(cleaned)
@@ -187,7 +222,7 @@ async def structured_json_generate(
187222
)
188223
if attempt > 0:
189224
logger.info("结构化 JSON 管线重试成功 (attempt=%d)", attempt)
190-
return instance
225+
return StructuredJSONGenerateResult(payload=instance)
191226
last_errors = parse_errors + schema_errors
192227
recorder.record_span(
193228
phase="schema_validated",
@@ -249,4 +284,8 @@ async def structured_json_generate(
249284
"total_attempts": total_attempts,
250285
},
251286
)
252-
return None
287+
return StructuredJSONGenerateResult(
288+
payload=None,
289+
failure_stage="parse_or_schema",
290+
errors=tuple(last_errors),
291+
)

application/analyst/services/tension_scoring_service.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
tension_scoring_payload_to_domain,
1515
tension_scoring_response_format,
1616
)
17-
from application.ai.structured_json_pipeline import structured_json_generate
17+
from application.ai.structured_json_pipeline import structured_json_generate_with_status
1818
from infrastructure.ai.generation_profiles import generation_config_from_profile
1919
from infrastructure.ai.prompt_contract import PromptContract
2020
from infrastructure.ai.prompt_gateway import PromptGatewayError, get_prompt_gateway
@@ -82,17 +82,22 @@ async def score_chapter(
8282
)
8383

8484
try:
85-
payload = await structured_json_generate(
85+
parsed = await structured_json_generate_with_status(
8686
llm=self._llm,
8787
prompt=prompt,
8888
config=config,
8989
schema_model=TensionScoringLlmPayload,
9090
)
91+
payload = parsed.payload
9192
except Exception as e:
9293
logger.warning("张力评分管线异常: %s", e)
9394
payload = None
95+
parsed = None
9496

9597
if payload is None:
98+
if parsed is not None and parsed.failure_stage == "parse_or_schema":
99+
logger.warning("张力评分解析失败,使用中性 50: %s", list(parsed.errors[:4]))
100+
return TensionDimensions.neutral()
96101
return TensionDimensions.unevaluated()
97102

98103
dims = tension_scoring_payload_to_domain(payload)

application/engine/dag/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ class NodeConfig(BaseModel):
199199
max_retries: int = Field(default=1, ge=0, le=5)
200200
timeout_seconds: int = Field(default=60, ge=10, le=600)
201201
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
202-
max_tokens: Optional[int] = Field(default=None, ge=100, le=16000)
202+
max_tokens: Optional[int] = Field(default=None, ge=100, le=120000)
203203

204204

205205
# ─── DAG 定义模型 ───

config/generation_profiles.yaml

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,81 @@
11
base_generation_profile:
22
provider_role: writing
3-
max_tokens: 16000
3+
max_tokens: 120000
44
temperature: 0.3
55
timeout_seconds: 60
66
retries: 1
77

88
memory_extraction:
99
extends: base_generation_profile
10-
max_tokens: 16000
10+
max_tokens: 120000
1111
temperature: 0.3
1212

1313
voice_style_analysis:
1414
extends: base_generation_profile
15-
max_tokens: 16000
15+
max_tokens: 120000
1616
temperature: 0.1
1717

1818
voice_baseline_analysis:
1919
extends: voice_style_analysis
20-
max_tokens: 16000
20+
max_tokens: 120000
2121
temperature: 0.1
2222

2323
chapter_summarizer:
2424
extends: base_generation_profile
25-
max_tokens: 16000
25+
max_tokens: 120000
2626
temperature: 0.7
2727

2828
tension_scoring:
2929
extends: base_generation_profile
30-
max_tokens: 16000
30+
max_tokens: 120000
3131
temperature: 0.3
3232

3333
tension_diagnosis:
3434
extends: base_generation_profile
35-
max_tokens: 16000
35+
max_tokens: 120000
3636
temperature: 0.2
3737

3838
review_json:
3939
extends: base_generation_profile
40-
max_tokens: 16000
40+
max_tokens: 120000
4141
temperature: 0.3
4242

4343
summary_act:
4444
extends: base_generation_profile
45-
max_tokens: 16000
45+
max_tokens: 120000
4646
temperature: 0.4
4747

4848
summary_volume:
4949
extends: base_generation_profile
50-
max_tokens: 16000
50+
max_tokens: 120000
5151
temperature: 0.5
5252

5353
summary_part:
5454
extends: base_generation_profile
55-
max_tokens: 16000
55+
max_tokens: 120000
5656
temperature: 0.45
5757

5858
summary_checkpoint:
5959
extends: base_generation_profile
60-
max_tokens: 16000
60+
max_tokens: 120000
6161
temperature: 0.5
6262

6363
planning_macro:
6464
extends: base_generation_profile
65-
max_tokens: 16000
65+
max_tokens: 120000
6666
temperature: 0.7
6767

6868
planning_repair:
6969
extends: base_generation_profile
70-
max_tokens: 16000
70+
max_tokens: 120000
7171
temperature: 0.5
7272

7373
planning_act:
7474
extends: base_generation_profile
75-
max_tokens: 16000
75+
max_tokens: 120000
7676
temperature: 0.7
7777

7878
planning_chapter_preplan:
7979
extends: base_generation_profile
80-
max_tokens: 16000
80+
max_tokens: 120000
8181
temperature: 0.65

domain/ai/services/llm_service.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
# domain/ai/services/llm_service.py
21
from abc import ABC, abstractmethod
32
from typing import AsyncIterator, Dict, Optional
43
from domain.ai.value_objects.prompt import Prompt
54
from domain.ai.value_objects.token_usage import TokenUsage
65

76

8-
DEFAULT_MAX_OUTPUT_TOKENS = 16000
7+
DEFAULT_MAX_OUTPUT_TOKENS = 120000
98

109

1110
class GenerationConfig:
@@ -22,14 +21,14 @@ def __init__(
2221
self.temperature = temperature
2322
self.response_format = response_format
2423
self.__post_init__()
25-
self.max_tokens = DEFAULT_MAX_OUTPUT_TOKENS
2624

2725
def __post_init__(self):
2826
"""验证配置参数"""
2927
if not (0.0 <= self.temperature <= 2.0):
3028
raise ValueError("Temperature must be between 0.0 and 2.0")
3129
if self.max_tokens <= 0:
3230
raise ValueError("max_tokens must be greater than 0")
31+
self.max_tokens = max(int(self.max_tokens), DEFAULT_MAX_OUTPUT_TOKENS)
3332

3433

3534
class GenerationResult:

engine/pipeline/base.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,8 +1155,9 @@ async def _score_tension_via_llm(self, ctx: PipelineContext) -> Optional[int]:
11551155
11561156
保留给子类/实验路径显式调用;默认管线不再把它作为兜底。
11571157
"""
1158+
import re
1159+
11581160
try:
1159-
import re
11601161
from application.ai_invocation.autopilot.factory import get_or_create_autopilot_helper_invoker
11611162
from application.ai_invocation.autopilot.helper_invoker import AutopilotHelperRequest
11621163
from infrastructure.ai.prompt_keys import TENSION_SCORING
@@ -1177,13 +1178,14 @@ async def _score_tension_via_llm(self, ctx: PipelineContext) -> Optional[int]:
11771178
config={"max_tokens": 10, "temperature": 0.3},
11781179
)
11791180
)
1180-
match = re.search(r'(\d+)', content)
1181-
if match:
1182-
score = int(match.group(1))
1183-
return min(score, 10) * 10 # 1-10 → 0-100
11841181
except Exception as e:
11851182
logger.warning("[%s] 单维张力评分失败: %s", ctx.novel_id, e)
1186-
return None
1183+
return None
1184+
match = re.search(r"(\d+)", str(content))
1185+
if not match:
1186+
return 50
1187+
score = int(match.group(1))
1188+
return min(score, 10) * 10 # 1-10 → 0-100
11871189

11881190
def _make_result(
11891191
self,

engine/runtime/daemon_host.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,10 +1585,13 @@ async def _score_tension(self, content: str) -> int:
15851585
config={"max_tokens": 5, "temperature": 0.1},
15861586
)
15871587
)
1588-
score = int(''.join(filter(str.isdigit, raw[:3])))
1589-
return max(1, min(10, score))
15901588
except Exception:
15911589
return 0
1590+
digits = "".join(filter(str.isdigit, str(raw)[:3]))
1591+
if not digits:
1592+
return 5
1593+
score = int(digits)
1594+
return max(1, min(10, score))
15921595

15931596
async def _stream_llm_with_stop_watch(
15941597
self,

engine/runtime/generation_token_policy.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22
from __future__ import annotations
33

44

5-
CHAPTER_PROSE_MAX_TOKENS = 16000
5+
from domain.ai.services.llm_service import DEFAULT_MAX_OUTPUT_TOKENS
6+
7+
8+
CHAPTER_PROSE_MAX_TOKENS = DEFAULT_MAX_OUTPUT_TOKENS

infrastructure/ai/config/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def __post_init__(self):
4242

4343
if self.default_max_tokens <= 0:
4444
raise ValueError("Max tokens must be positive")
45-
self.default_max_tokens = DEFAULT_MAX_OUTPUT_TOKENS
45+
self.default_max_tokens = max(int(self.default_max_tokens), DEFAULT_MAX_OUTPUT_TOKENS)
4646

4747
if self.timeout_seconds <= 0:
4848
raise ValueError("timeout_seconds must be positive")

0 commit comments

Comments
 (0)