fix(ingest): 入库失败去静默化——真实原因落 error_msg

- _fail(): status=failed + error_msg 落库 + exception() 记日志(旧版只写
  metadata 且 UPDATE 自身异常被吞 → 文档永停 pending 无从排障)
- dbname 钩子缺失时兜底落错(脱离服务进程的裸任务不再无日志消失)
- 文本主路径:embedding 返回空 / VDB upsert 非 SUCCEEDED 直接 failed+原因,
  不再 except:pass 留 vector_id 空壳 done
- chunks INSERT 后补 COMMIT(纯 SELECT 后不 COMMIT 防 MDL 锁惯例)
This commit is contained in:
ymq 2026-09-03 16:43:58 +08:00
parent 98389da60b
commit bc2849549d

View File

@ -10,6 +10,7 @@ ingest_one(doc_id, kb_id, file_name, ext_l, real_path):
避免 UI 上传与 /rag/api 对外上传两份入库实现分叉 避免 UI 上传与 /rag/api 对外上传两份入库实现分叉
""" """
import json, os, base64, subprocess import json, os, base64, subprocess
from appPublic.log import exception
from ahserver.serverenv import ServerEnv from ahserver.serverenv import ServerEnv
from sqlor.dbpools import DBPools from sqlor.dbpools import DBPools
from appPublic.streamhttpclient import StreamHttpClient from appPublic.streamhttpclient import StreamHttpClient
@ -21,11 +22,40 @@ def _dbname():
return _env.get_module_dbname('rag') return _env.get_module_dbname('rag')
async def _fail(db, _dbn, doc_id, reason):
"""入库失败落库:状态置 failed + error_msg 记录真实原因(禁静默 pending"""
import traceback
tb = traceback.format_exc()
msg = reason + (' | ' + tb[-400:] if 'Traceback' in tb else '')
exception(f"ingest_one[{doc_id}] failed: {msg}")
try:
async with db.sqlorContext(_dbn) as sor:
await sor.sqlExe(
"UPDATE rag_documents SET status='failed', error_msg=${e}$, updated_at=NOW() WHERE id=${id}$",
{"id": doc_id, "e": msg[:600]})
await sor.sqlExe("COMMIT", {})
except Exception:
pass
async def ingest_one(doc_id, kb_id, file_name, ext_l, real_path): async def ingest_one(doc_id, kb_id, file_name, ext_l, real_path):
db = DBPools() db = DBPools()
# 库名必须经宿主的 get_module_dbname 映射ragserver→'rag'pipeline-app→'pipeline' # 库名必须经宿主的 get_module_dbname 映射ragserver→'rag'pipeline-app→'pipeline'
# 后台任务无 request/env用注入的全局函数解析禁硬编码库名 # 后台任务无 request/env用注入的全局函数解析禁硬编码库名
_dbn = _dbname() # 钩子缺失(如脱离服务进程的裸任务)必须落错,不能静默吞掉
try:
_dbn = _dbname()
except Exception as e:
exception(f"ingest_one[{doc_id}]: get_module_dbname 钩子不可用: {e}")
try:
async with db.sqlorContext('pipeline') as sor:
await sor.sqlExe(
"UPDATE rag_documents SET status='failed', error_msg=${e}$, updated_at=NOW() WHERE id=${id}$",
{"id": doc_id, "e": ('dbname 解析失败: ' + str(e))[:600]})
await sor.sqlExe("COMMIT", {})
except Exception:
pass
return
# 读知识库向量引擎: # 读知识库向量引擎:
# bge-m3 → 在线文本(阿里, 1024维) # bge-m3 → 在线文本(阿里, 1024维)
@ -301,20 +331,23 @@ async def ingest_one(doc_id, kb_id, file_name, ext_l, real_path):
# 在线文本向量化vl引擎→qwen3-vl原生bge-m3→在线兼容GPU CLIP 时代的老路径已移除) # 在线文本向量化vl引擎→qwen3-vl原生bge-m3→在线兼容GPU CLIP 时代的老路径已移除)
embeddings = await _online_text_embed(chunks) embeddings = await _online_text_embed(chunks)
embeddings = [e for e in embeddings if e] or [] embeddings = [e for e in embeddings if e] or []
if not embeddings:
await _fail(db, _dbn, doc_id,
'embedding 返回空(在线引擎不可达/未配置,检查 rag_engine_configs 与 VDB_BASE=' + VDB_BASE + '')
return
vector_ids = [] client2 = StreamHttpClient()
if embeddings: await ensure_vdb_collection(client2, kb_id)
try: vdb_data = {"colname": kb_id, "data": [
client2 = StreamHttpClient() {"id": doc_id + "_c" + str(i), "vector": emb, "text": chunks[i]}
await ensure_vdb_collection(client2, kb_id) for i, emb in enumerate(embeddings)]}
vdb_data = {"colname": kb_id, "data": [ resp3 = await client2.request('POST', VDB_BASE + '/v1/upsert', json=vdb_data)
{"id": doc_id + "_c" + str(i), "vector": emb, "text": chunks[i]} vdb_ret = json.loads(resp3)
for i, emb in enumerate(embeddings)]} if vdb_ret.get('status') != 'SUCCEEDED':
resp3 = await client2.request('POST', VDB_BASE + '/v1/upsert', json=vdb_data) await _fail(db, _dbn, doc_id,
if json.loads(resp3).get('status') == 'SUCCEEDED': 'VDB upsert 失败 @ ' + VDB_BASE + ': ' + str(vdb_ret)[:300])
vector_ids = [doc_id + "_c" + str(i) for i in range(len(embeddings))] return
except: vector_ids = [doc_id + "_c" + str(i) for i in range(len(embeddings))]
pass
async with db.sqlorContext(_dbn) as sor: async with db.sqlorContext(_dbn) as sor:
for i, chunk_text in enumerate(chunks): for i, chunk_text in enumerate(chunks):
@ -324,14 +357,10 @@ async def ingest_one(doc_id, kb_id, file_name, ext_l, real_path):
"VALUES (${id}$, ${doc_id}$, ${kb_id}$, ${idx}$, ${content}$, ${vid}$, NOW())", "VALUES (${id}$, ${doc_id}$, ${kb_id}$, ${idx}$, ${content}$, ${vid}$, NOW())",
{"id": doc_id + "_c" + str(i), "doc_id": doc_id, "kb_id": kb_id, {"id": doc_id + "_c" + str(i), "doc_id": doc_id, "kb_id": kb_id,
"idx": i, "content": chunk_text[:2000], "vid": vid}) "idx": i, "content": chunk_text[:2000], "vid": vid})
await sor.sqlExe("COMMIT", {})
chunks_n = len(chunks) chunks_n = len(chunks)
except Exception as e: except Exception as e:
try: await _fail(db, _dbn, doc_id, 'ingest 异常: ' + repr(e)[:300])
async with db.sqlorContext(_dbn) as sor:
await sor.sqlExe(
"UPDATE rag_documents SET status='failed', metadata=${meta}$, updated_at=NOW() WHERE id=${id}$",
{"id": doc_id, "meta": json.dumps({"error": str(e)[:300]}, ensure_ascii=False)})
except: pass
return return
# --- finalize: mark document done + update KB chunk counts --- # --- finalize: mark document done + update KB chunk counts ---