feat(server): 分块并发调用模型,配置可从仓库根目录的 .env 读取

- extract:分块并发调用(每个任务最多 4 路),结果按分块顺序合并
- config:在 server/ 下运行时也读取仓库根目录的 .env(和 Compose 的 env_file 一致)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-03 18:55:59 +08:00
parent be62440531
commit ee4bb23488
2 changed files with 22 additions and 12 deletions

View File

@@ -6,6 +6,7 @@
from __future__ import annotations
import asyncio
import re
from dataclasses import dataclass, field
from typing import Any
@@ -17,6 +18,7 @@ from bid_doctor.document.pipeline.text_layer import Sentence, norm
PROMPT_ID, PROMPT_VERSION = "extract.substantive", 1
CHUNK_CHARS = 6000
CONCURRENCY = 4 # 每个解析任务同时调用模型的分块数;TODO(路由表):按「供应商 × 租户」统一限流
INVALID = re.compile(r"投标无效|响应无效|无效投标|作无效处理|按无效|否决其投标|否决投标|不予受理")
RULE_NOTE = re.compile(r"(★|*|\*|斜体|下划线).{0,30}(实质性|无效|否决)|(实质性).{0,20}(★|*|\*|斜体|下划线)")
DECLARED = re.compile(r"★[^。;]{0,12}?共\s*(\d+)\s*[条项处]")
@@ -125,20 +127,28 @@ async def run(ctx: TenantContext, job_id: UUID, sentences: list[Sentence]) -> tu
note_keys = {n.sentence.key for n in notes}
declared = {n.marker for n in notes}
out: list[Extracted] = []
for chunk in _chunks(sentences):
by_id = {_sid(s): s for s in chunk}
chunks = _chunks(sentences)
gate = asyncio.Semaphore(CONCURRENCY)
async def call(chunk: list[Sentence]) -> dict[str, Any]:
user = (
"## 标注规则说明\n" + note_text + "\n\n## 待审分块\n" + "\n".join(f"[{_sid(s)}] {s.marked}" for s in chunk)
)
data = await ai.run_json(
ctx,
job_type="parse_job",
job_id=job_id,
task="extract.substantive",
prompt_id=PROMPT_ID,
version=PROMPT_VERSION,
user=user,
)
async with gate:
return await ai.run_json(
ctx,
job_type="parse_job",
job_id=job_id,
task="extract.substantive",
prompt_id=PROMPT_ID,
version=PROMPT_VERSION,
user=user,
)
# 分块并发调用模型(架构 §3.4);结果按分块顺序处理,清单顺序和原文一致
results = await asyncio.gather(*(call(c) for c in chunks))
for chunk, data in zip(chunks, results, strict=True):
by_id = {_sid(s): s for s in chunk}
for it in data.get("items") or []:
if not isinstance(it, dict):
continue

View File

@@ -13,7 +13,7 @@ DEV_SECRET = "dev-insecure-secret-change-me-0000000000000000" # noqa: S105 -
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="BD_", env_file=".env", extra="ignore")
model_config = SettingsConfigDict(env_prefix="BD_", env_file=(".env", "../.env"), extra="ignore")
env: Literal["dev", "test", "prod"] = "dev"