基于 GPT-4V/Claude 3 多模态大模型实现图片内容识别与结构化提取
本章带你完成 阶段 4:「图识」多模态内容识别器 项目: 基于 GPT-4V/Claude 3 多模态大模型 构建一个具备图片内容识别、结构化输出、多源集成的智能内容提取系统。
从图片输入到 Markdown 输出的两阶段分离架构
# 复制本段到 vision_text_two_stage_demo.py 可直接运行验证
# 两阶段分离的核心思想:
# - Vision Step:只产“结构化 JSON”(短、稳定、可缓存)
# - Text Step:只消费 JSON 生成 Markdown(成本低、模板可控)
# - Contract/Schema:夹在中间,保证不合规 JSON 不能进入文本生成
#
# 这个示例关注“工程关键点”:
# - 可观测:trace_id + 分段耗时 + 错误分类(stage=vision/contract/text)
# - 可控:可开关跳过 Vision 用 mock JSON(证明解耦)
# - 可替换:Vision/Text 模型都可替换,只需改 TongyiVision/TongyiWriter 初始化的 model
# - 安全:只读图片,不会创建/覆盖任何图片文件
import json
import os
import time
import uuid
import hashlib
from dataclasses import dataclass
from typing import Any, Dict, Optional, Callable, Tuple
import re
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import HumanMessage
# -------------------------
# 0) Trace:trace_id + 分段耗时
# -------------------------
class Trace:
def __init__(self, name: str):
self.trace_id = f"t-{uuid.uuid4().hex[:8]}"
self.name = name
self.t0 = time.time()
self.events = []
def mark(self, stage: str, ok: bool, meta: Optional[Dict[str, Any]] = None) -> None:
self.events.append({
"stage": stage,
"ok": ok,
"ms": int((time.time() - self.t0) * 1000),
"meta": meta or {},
})
def dump(self) -> Dict[str, Any]:
return {"trace_id": self.trace_id, "name": self.name, "events": self.events}
def classify_error(e: Exception) -> str:
s = str(e).lower()
if "not found" in s or "不存在" in s or "path" in s or "image_not_found" in s:
return "input_error"
if "contract" in s or "schema" in s or "missing" in s or "contract_invalid" in s:
return "contract_error"
if "timeout" in s or "status_code" in s or "provider" in s:
return "provider_error"
return "system_error"
# -------------------------
# 1) 输入治理(只读):限制大小 + 生成稳定 cache_key
# -------------------------
MAX_IMAGE_BYTES = 5 * 1024 * 1024 # 5MB
VISION_CACHE: Dict[str, Dict[str, Any]] = {} # 生产:Redis + TTL
def _sha256_bytes(b: bytes) -> str:
return hashlib.sha256(b).hexdigest()
def load_image_bytes(image_ref: str) -> Tuple[bytes, Dict[str, Any]]:
"""只读加载图片 bytes。
- 本地路径:读取文件 bytes
- URL:离线示例不下载,仅用 URL 文本作为稳定输入(演示 cache_key/链路)
"""
if image_ref.startswith("http://") or image_ref.startswith("https://"):
b = image_ref.encode("utf-8")
return b, {"type": "url", "bytes": len(b)}
if not os.path.exists(image_ref):
raise FileNotFoundError(f"image_not_found: {image_ref}")
size = os.path.getsize(image_ref)
if size > MAX_IMAGE_BYTES:
raise ValueError(f"image_too_large: {size} > {MAX_IMAGE_BYTES}")
with open(image_ref, "rb") as f:
b = f.read()
return b, {"type": "file", "bytes": len(b), "path": image_ref}
# -------------------------
# 2) Vision Step:真实调用 qwen-vl-plus(只产结构化 JSON,稳定/短/可缓存)
# -------------------------
@dataclass
class VisionJSON:
topic: str
summary: str
entities: list[str]
need_more_info: list[str]
def to_dict(self) -> Dict[str, Any]:
return {
"topic": self.topic,
"summary": self.summary,
"entities": self.entities,
"need_more_info": self.need_more_info,
}
VISION_CONTRACT = (
"你是严谨的企业识图助手。请根据图片提取结构化信息,并严格只输出 JSON。\n"
"【强制规则】\n"
"1. 只输出 JSON,不要输出任何其他文字/Markdown/解释。\n"
"2. 看不清/不确定的信息,必须放入 need_more_info,绝对不要猜测。\n"
"3. entities 只列出图片中明确存在的信息。\n"
"【输出格式】\n"
'{"topic": string, "summary": string, "entities": [string], "need_more_info": [string]}'
)
def _extract_json(text: str) -> str:
t = (text or "").strip()
if t.startswith("```"):
t = re.sub(r"^```(?:json)?\s*", "", t, flags=re.IGNORECASE)
t = re.sub(r"\s*```$", "", t)
m = re.search(r"\{.*\}", t, flags=re.S)
return m.group(0) if m else t
def _resp_text(resp: Any) -> str:
c = getattr(resp, "content", None)
if isinstance(c, str):
return c
if isinstance(c, list) and c and isinstance(c[0], dict) and "text" in c[0]:
return c[0].get("text") or ""
return str(c or "")
def _to_tongyi_image(image_ref: str) -> str:
if image_ref.startswith("http://") or image_ref.startswith("https://") or image_ref.startswith("file://"):
return image_ref
return f"file://{os.path.abspath(image_ref)}"
class TongyiVision:
"""
通义千问 Vision 模型封装类
作用:将图片识别成结构化的 JSON 数据
"""
def __init__(self, model: str = "qwen-vl-plus"):
# 初始化通义千问多模态模型
# temperature=0 表示输出确定性最高,避免随机性
self.llm = ChatTongyi(model=model, temperature=0)
def __call__(self, image_ref: str, trace: Trace) -> Dict[str, Any]:
"""
核心识图函数(可调用对象)
参数:
image_ref: 图片路径或URL
trace: 追踪对象,用于记录执行过程
返回:
Dict[str, Any]: 结构化的JSON数据,包含 topic、summary、entities 等字段
"""
# 构建多模态消息:文本Prompt + 图片
msg = HumanMessage(content=[
{"type": "text", "text": VISION_CONTRACT}, # VISION_CONTRACT 是预定义的识图Prompt
{"type": "image", "image": _to_tongyi_image(image_ref)}, # 将图片路径转换为通义千问支持的格式
])
# 调用通义千问多模态模型进行识图
resp = self.llm.invoke([msg])
# 提取响应文本(可能包含 JSON 或 Markdown 代码块)
text = _resp_text(resp)
# 从响应文本中提取纯 JSON 字符串,并解析为 Python 字典
obj = json.loads(_extract_json(text))
# 记录到 trace:vision 阶段执行成功
trace.mark("vision", True, {"model": "qwen-vl-plus"})
return obj
def vision_step(image_ref: str, vision_fn: Callable[[str, Trace], Dict[str, Any]], trace: Trace) -> Dict[str, Any]:
"""
Vision 步骤编排函数(带缓存优化)
参数说明:
image_ref: 图片路径或URL(如 "../data/img.png" 或 "https://example.com/img.jpg")
vision_fn: Vision 函数对象(通常是 TongyiVision 实例)
这是一个可调用对象,签名为 (image_ref: str, trace: Trace) -> Dict[str, Any]
例如:vision = TongyiVision(model="qwen-vl-plus")
然后 vision_fn 就是这个 vision 对象
trace: 追踪对象,用于记录整个流程的执行情况
返回:
Dict[str, Any]: Vision 模型识别出的结构化 JSON 数据
工作流程:
1. 加载图片字节 → 2. 计算缓存key → 3. 检查缓存 → 4. 调用Vision模型 → 5. 缓存结果
"""
# 步骤1:加载图片字节数据(只读,不修改文件)
# 返回:image_bytes(图片的二进制数据)、meta(元信息,如文件大小、类型)
image_bytes, meta = load_image_bytes(image_ref)
# 步骤2:计算图片的 SHA256 哈希值作为缓存 key
# 相同图片 → 相同哈希 → 可以复用之前的识别结果
cache_key = _sha256_bytes(image_bytes)
# 记录预检阶段:图片加载成功,缓存key已生成
trace.mark("precheck", True, {"input": meta, "cache_key": cache_key[:10]})
# 步骤3:检查缓存(生产环境通常用 Redis,这里用内存字典演示)
if cache_key in VISION_CACHE:
# 缓存命中:直接返回之前的识别结果,不再调用多模态模型(节省成本)
trace.mark("vision_cache", True, {"hit": True})
return VISION_CACHE[cache_key]
# 步骤4:缓存未命中,调用 Vision 函数进行真实识图
# 这里 vision_fn 是一个可调用对象(实现了 __call__ 方法的类实例)
# 调用方式:vision_fn(image_ref, trace)
# 等价于:vision.__call__(image_ref, trace)
# 实际执行的是 TongyiVision.__call__ 方法,会调用通义千问多模态API
out = vision_fn(image_ref, trace)
# 步骤5:将识别结果存入缓存,下次相同图片可直接复用
VISION_CACHE[cache_key] = out
# 记录缓存状态:未命中,已将新结果存入缓存
trace.mark("vision_cache", True, {"hit": False})
return out
# -------------------------
# 3) Contract Step:契约校验 + 可控修复(只修结构,不编造事实)
# -------------------------
REQUIRED_KEYS = ("topic", "summary", "entities", "need_more_info")
def contract_validate_and_repair(vision_json: Dict[str, Any], trace: Trace) -> Dict[str, Any]:
repaired = dict(vision_json or {})
# 缺字段:只补默认值
for k in REQUIRED_KEYS:
if k not in repaired:
repaired[k] = [] if k in ("entities", "need_more_info") else ""
# 类型修复:只做结构修复
if not isinstance(repaired["topic"], str):
repaired["topic"] = str(repaired["topic"])
if not isinstance(repaired["summary"], str):
repaired["summary"] = str(repaired["summary"])
if not isinstance(repaired["entities"], list):
repaired["entities"] = [str(repaired["entities"])]
repaired["entities"] = [str(x) for x in repaired["entities"]][:20]
if not isinstance(repaired["need_more_info"], list):
repaired["need_more_info"] = [str(repaired["need_more_info"])] if repaired["need_more_info"] else []
repaired["need_more_info"] = [str(x) for x in repaired["need_more_info"]][:20]
# 最终校验:仍不合规就拦截(避免 Text 被污染)
for k in REQUIRED_KEYS:
if repaired[k] is None:
raise ValueError(f"contract_invalid: {k}=None")
trace.mark("contract", True, {"repaired": repaired != (vision_json or {})})
return repaired
# -------------------------
# 4) Text Step:只消费 JSON -> Markdown(模板可控/成本低)
# -------------------------
def build_markdown_prompt(v: Dict[str, Any]) -> str:
return (
"你是企业知识助手。请用 Markdown 输出一个固定结构的卡片:\n"
"# 标题\n"
"## 结论(1-2 句)\n"
"## 证据(引用 vision summary)\n"
"## 关键实体(列表)\n"
"## 下一步建议(2 条)\n\n"
f"topic={v.get('topic','')}\n"
f"summary={v.get('summary','')}\n"
f"entities={v.get('entities',[])}\n"
)
class TongyiWriter:
def __init__(self, model: str = "qwen-plus"):
self.llm = ChatTongyi(model=model, temperature=0.2)
def __call__(self, prompt: str, trace: Trace) -> str:
resp = self.llm.invoke([HumanMessage(content=prompt)])
trace.mark("text", True, {"model": "qwen-plus"})
return _resp_text(resp).strip()
def text_step(vision_json: Dict[str, Any], writer_fn: Callable[[str, Trace], str], trace: Trace) -> str:
prompt = build_markdown_prompt(vision_json)
return writer_fn(prompt, trace)
# -------------------------
# 5) 编排:纯 Python 兜底 +(可选)LangChain RunnableLambda
# -------------------------
def run_pipeline(image_ref: str, vision_fn, writer_fn, trace: Trace) -> Dict[str, Any]:
"""
执行两阶段分离的视觉-文本处理管道
Args:
image_ref (str): 图片引用,可以是本地路径或URL
vision_fn (Callable): 视觉处理函数,负责将图片转换为结构化JSON
writer_fn (Callable): 文本生成函数,负责将JSON转换为Markdown
trace (Trace): 追踪对象,用于记录执行过程和性能指标
Returns:
Dict[str, Any]: 包含以下键的字典:
- trace: 完整的执行追踪信息
- vision_json: 视觉识别结果(结构化JSON),失败时为None
- markdown: 生成的Markdown文本,失败时为None
- error: 错误信息,成功时为None,失败时包含错误类型和消息
Pipeline流程:
1. Vision Step: 图片 → 结构化JSON (topic/summary/entities/need_more_info)
2. Contract Step: JSON格式校验和修复,确保必需字段存在
3. Text Step: JSON → Markdown报告
异常处理:
- 任何步骤失败都会记录到trace中
- 返回统一的错误格式,包含错误分类和详细信息
- 保证即使失败也能返回trace信息用于问题定位
"""
try:
# 第一阶段:视觉识别,将图片转换为结构化JSON
vision_json = vision_step(image_ref, vision_fn, trace)
# 第二阶段:契约校验,确保JSON格式符合预期
vision_json = contract_validate_and_repair(vision_json, trace)
# 第三阶段:文本生成,将JSON转换为Markdown报告
md = text_step(vision_json, writer_fn, trace)
return {"trace": trace.dump(), "vision_json": vision_json, "markdown": md, "error": None}
except Exception as e:
# 记录错误信息到追踪中
trace.mark("error", False, {"type": classify_error(e), "msg": str(e)})
return {"trace": trace.dump(), "vision_json": None, "markdown": None, "error": {"type": classify_error(e), "msg": str(e)}}
def build_langchain_chain(vision_fn, writer_fn):
"""
构建LangChain链式处理器,提供与run_pipeline相同功能的LangChain接口
Args:
vision_fn (Callable): 视觉处理函数,负责将图片转换为结构化JSON
writer_fn (Callable): 文本生成函数,负责将JSON转换为Markdown
Returns:
RunnableLambda | None:
- 如果安装了LangChain,返回RunnableLambda对象
- 如果未安装LangChain,返回None
功能说明:
- 提供LangChain生态系统的兼容接口
- 内部封装run_pipeline函数,保持相同的处理逻辑
- 自动创建Trace对象进行执行追踪
- 支持LangChain的链式调用和组合操作
使用示例:
```python
# 创建链式处理器
chain = build_langchain_chain(vision_fn, writer_fn)
# 直接调用
result = chain.invoke("path/to/image.jpg")
# 与其他LangChain组件组合
from langchain_core.runnables import RunnablePassthrough
full_chain = {"image_ref": RunnablePassthrough()} | chain
```
设计优势:
- 兼容LangChain生态系统,便于与其他组件集成
- 保持纯Python兜底实现,不强制依赖LangChain
- 统一的错误处理和追踪机制
- 支持LangChain的流式处理和批处理特性
"""
try:
from langchain_core.runnables import RunnableLambda
def _run(image_ref: str) -> Dict[str, Any]:
"""
内部执行函数,包装run_pipeline以适配LangChain接口
Args:
image_ref (str): 图片引用路径或URL
Returns:
Dict[str, Any]: 与run_pipeline相同的返回格式
"""
# 创建专用的追踪对象
trace = Trace("vision_text_two_stage")
# 调用核心处理管道
return run_pipeline(image_ref, vision_fn, writer_fn, trace)
# 返回LangChain包装器
return RunnableLambda(_run)
except Exception:
# LangChain未安装时的兜底处理
return None
if __name__ == "__main__":
# ========================================
# 使用说明
# ========================================
# 1. 设置环境变量:export DASHSCOPE_API_KEY="你的API Key"
# 2. 准备图片:将图片路径替换为你的本地图片或URL
# 3. 运行代码:python vision_text_two_stage_demo.py
# 4. 查看输出:包含 trace_id、vision_json、markdown 三部分
# 图片路径(支持本地路径或URL)
image_ref = "../data/img.png" # 替换成你的本地图片路径或 URL
# 检查API Key是否设置
if not os.getenv("DASHSCOPE_API_KEY"):
raise ValueError("请先设置环境变量 DASHSCOPE_API_KEY")
# 初始化 Vision 模型(负责识图,输出结构化JSON)
# qwen-vl-plus: 性价比高,适合大批量处理
# qwen-vl-max: 准确率更高,适合高精度场景
vision = TongyiVision(model="qwen-vl-plus")
# 初始化 Text 模型(负责基于JSON生成Markdown报告)
# qwen-plus: 成本低,生成质量稳定
# qwen-max: 生成质量更高,适合复杂报告
writer = TongyiWriter(model="qwen-plus")
# 构建 LangChain Runnable Chain
# 优势:可与其他 LangChain 组件无缝集成(如 LCEL、Agent、Memory)
chain = build_langchain_chain(vision, writer)
if chain is None:
raise RuntimeError("LangChain 未正确安装,请运行: pip install langchain langchain-community")
# 执行完整流程:图片 → Vision JSON → Schema校验 → Markdown报告
# 返回结果包含:
# - trace: 完整链路追踪(trace_id、各阶段耗时、成功/失败状态)
# - vision_json: Vision模型识别的结构化数据
# - markdown: Text模型生成的学习卡片
# - error: 错误信息(如果有)
result = chain.invoke(image_ref)
# 格式化输出结果
print("=" * 60)
print("LangChain 两阶段分离架构 - 执行结果")
print("=" * 60)
print(json.dumps(result, ensure_ascii=False, indent=2))
# 输出关键指标
if result.get("error") is None:
print("\n" + "=" * 60)
print("✅ 执行成功")
print("=" * 60)
print(f"Trace ID: {result['trace']['trace_id']}")
print(f"识别主题: {result['vision_json'].get('topic', 'N/A')}")
print(f"关键实体数: {len(result['vision_json'].get('entities', []))}")
print(f"待确认信息: {len(result['vision_json'].get('need_more_info', []))}")
else:
print("\n" + "=" * 60)
print("❌ 执行失败")
print("=" * 60)
print(f"错误类型: {result['error'].get('type', 'unknown')}")
print(f"错误信息: {result['error'].get('msg', 'N/A')}")pip install streamlit dashscope langchain langchain-community pillow pandas openpyxlexport DASHSCOPE_API_KEY="你的Key"streamlit run main.py项目四:学习卡片生成器/
├── main.py # Streamlit 主程序:上传图片 + 展示结果
├── chains.py # LangChain 编排:vision -> card
├── vision.py # 通义多模态识图(qwen-vl-plus)
├── prompts.py # 输出合同(JSON schema / markdown 模板)
└── requirements.txt # 依赖用户上传图片
↓
Vision Step(qwen-vl-plus):识图 -> JSON(summary/entities/topic)
↓
Text Step(qwen-plus):基于 JSON 生成学习卡片(markdown)
↓
输出到页面(复制/保存)"""
Streamlit 主程序:上传图片 + 展示结果
运行方式:streamlit run main.py
"""
import streamlit as st
import os
from chains import build_vision_card_chain
from PIL import Image
import tempfile
# 页面配置
st.set_page_config(
page_title="图识 - 多模态学习卡片生成器",
page_icon="🖼️",
layout="wide"
)
# 标题
st.markdown("""
<h1 style='text-align: center; color: #60a5fa;'>
🖼️ 图识 - 多模态学习卡片生成器
</h1>
<p style='text-align: center; color: #94a3b8;'>
基于 LangChain + 通义千问多模态,一键识别图片生成学习卡片
</p>
""", unsafe_allow_html=True)
# 检查 API Key
if not os.getenv("DASHSCOPE_API_KEY"):
st.error("⚠️ 请先设置环境变量 DASHSCOPE_API_KEY")
st.code("export DASHSCOPE_API_KEY='你的API Key'")
st.stop()
# 侧边栏配置
with st.sidebar:
st.markdown("### ⚙️ 系统配置")
vision_model = st.selectbox(
"Vision 模型",
["qwen-vl-plus", "qwen-vl-max"],
help="qwen-vl-plus 性价比高,qwen-vl-max 准确率更高"
)
text_model = st.selectbox(
"Text 模型",
["qwen-plus", "qwen-max"],
help="qwen-plus 成本低,qwen-max 质量更高"
)
st.markdown("### 📊 今日统计")
if 'stats' not in st.session_state:
st.session_state.stats = {"total": 0, "success": 0, "failed": 0}
col1, col2 = st.columns(2)
with col1:
st.metric("总请求", st.session_state.stats["total"])
with col2:
success_rate = (st.session_state.stats["success"] / max(st.session_state.stats["total"], 1)) * 100
st.metric("成功率", f"{success_rate:.1f}%")
# 主界面
col1, col2 = st.columns([1, 1])
with col1:
st.markdown("### 📤 上传图片")
uploaded_file = st.file_uploader(
"选择图片文件",
type=["png", "jpg", "jpeg", "webp"],
help="支持 PNG、JPG、JPEG、WebP 格式"
)
if uploaded_file:
st.image(uploaded_file, caption="上传的图片", use_column_width=True)
# 识别按钮
if st.button("🚀 开始识别", type="primary", disabled=not uploaded_file):
with st.spinner("正在识别图片内容..."):
# 保存上传的图片到临时文件
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{uploaded_file.name.split('.')[-1]}") as tmp_file:
tmp_file.write(uploaded_file.getvalue())
tmp_path = tmp_file.name
try:
# 构建 LangChain Chain
chain = build_vision_card_chain(
vision_model=vision_model,
text_model=text_model
)
# 执行识别
result = chain.invoke({"image_path": tmp_path})
# 更新统计
st.session_state.stats["total"] += 1
if result.get("error"):
st.error(f"❌ 识别失败: {result['error']}")
st.session_state.stats["failed"] += 1
if result.get("trace"):
with st.expander("🔍 Trace 详情"):
st.json(result["trace"])
else:
st.session_state.stats["success"] += 1
st.session_state.current_result = result
st.success(f"✅ 识别完成!Trace ID: {result['trace']['trace_id']}")
st.rerun()
finally:
# 清理临时文件
if os.path.exists(tmp_path):
os.unlink(tmp_path)
with col2:
st.markdown("### 📋 识别结果")
if 'current_result' in st.session_state:
result = st.session_state.current_result
# Vision JSON
with st.expander("🔍 Vision JSON(原始结果)", expanded=False):
st.json(result["vision_json"])
# 学习卡片
st.markdown("### 📝 学习卡片")
st.markdown(result["markdown"])
# 导出按钮
st.markdown("### 📤 导出数据")
col1, col2, col3 = st.columns(3)
with col1:
st.download_button(
"📄 下载 JSON",
data=str(result["vision_json"]),
file_name="result.json",
mime="application/json"
)
with col2:
st.download_button(
"📝 下载 Markdown",
data=result["markdown"],
file_name="card.md",
mime="text/markdown"
)
with col3:
# Trace 信息
with st.expander("🔍 Trace 详情"):
st.json(result["trace"])"""
LangChain 编排:vision -> card
核心逻辑:两阶段分离架构
"""
import time
import hashlib
from datetime import datetime
from typing import Dict, Any
from langchain_core.runnables import RunnableLambda
from vision import route_image_kind, recognize_image_to_json
from prompts import build_card_prompt
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import HumanMessage
class Trace:
"""追踪器:记录执行过程"""
def __init__(self, name: str):
self.trace_id = f"t-{datetime.now().strftime('%Y%m%d%H%M%S')}-{hashlib.md5(str(time.time()).encode()).hexdigest()[:8]}"
self.name = name
self.start_time = time.time()
self.events = []
def mark(self, stage: str, ok: bool, meta: Dict[str, Any] = None):
"""记录阶段"""
self.events.append({
"stage": stage,
"ok": ok,
"timestamp": time.time(),
"duration_ms": int((time.time() - self.start_time) * 1000),
"meta": meta or {}
})
def dump(self) -> Dict[str, Any]:
"""导出追踪信息"""
return {
"trace_id": self.trace_id,
"name": self.name,
"total_ms": int((time.time() - self.start_time) * 1000),
"events": self.events,
"ok": all(e["ok"] for e in self.events)
}
def vision_card_pipeline(image_path: str, vision_model: str, text_model: str) -> Dict[str, Any]:
"""
完整流程:图片 -> Vision JSON -> 学习卡片
"""
trace = Trace("vision_card_pipeline")
try:
# 步骤1:路由判断
kind = route_image_kind(image_path)
trace.mark("route", True, {"kind": kind})
# 步骤2:Vision 识图
vision_json = recognize_image_to_json(image_path, kind, vision_model, trace)
trace.mark("vision", True, {"model": vision_model})
# 步骤3:生成学习卡片
text_llm = ChatTongyi(model=text_model, temperature=0.2)
prompt = build_card_prompt(vision_json, kind)
response = text_llm.invoke([HumanMessage(content=prompt)])
# 提取响应文本(处理可能的列表格式)
if isinstance(response.content, str):
markdown = response.content.strip()
elif isinstance(response.content, list):
markdown = ""
for item in response.content:
if isinstance(item, str):
markdown = item.strip()
break
elif isinstance(item, dict) and "text" in item:
markdown = item["text"].strip()
break
else:
markdown = str(response.content).strip()
trace.mark("text", True, {"model": text_model})
return {
"trace": trace.dump(),
"vision_json": vision_json,
"markdown": markdown,
"error": None
}
except Exception as e:
trace.mark("error", False, {"msg": str(e)})
return {
"trace": trace.dump(),
"vision_json": None,
"markdown": None,
"error": str(e)
}
def build_vision_card_chain(vision_model: str = "qwen-vl-plus", text_model: str = "qwen-plus"):
"""
构建 LangChain Runnable Chain
"""
def _run(inputs: Dict[str, Any]) -> Dict[str, Any]:
image_path = inputs["image_path"]
return vision_card_pipeline(image_path, vision_model, text_model)
return RunnableLambda(_run)"""
通义多模态识图(qwen-vl-plus)
核心功能:路由判断 + 图片识别
"""
import json
import re
import os
from typing import Dict, Any
from PIL import Image
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import HumanMessage
from prompts import VISION_PROMPTS
def route_image_kind(image_path: str) -> str:
"""
智能路由判断:根据图片特征判断类型
规则:
- 宽高比 > 1.5 → chart(图表)
- 宽高比 < 0.7 → text(文本)
- 其他 → table(表格)
"""
img = Image.open(image_path)
width, height = img.size
aspect_ratio = width / height
if aspect_ratio > 1.5:
return "chart"
elif aspect_ratio < 0.7:
return "text"
else:
return "table"
def recognize_image_to_json(image_path: str, kind: str, model: str, trace) -> Dict[str, Any]:
"""
调用通义千问多模态模型识别图片
参数:
image_path: 图片路径
kind: 图片类型(chart/table/text)
model: 模型名称(qwen-vl-plus/qwen-vl-max)
trace: 追踪对象
返回:
Dict: 结构化 JSON 数据
"""
# 初始化模型
llm = ChatTongyi(model=model, temperature=0)
# 获取对应的 Prompt
prompt = VISION_PROMPTS.get(kind, VISION_PROMPTS["text"])
# 构建消息
message = HumanMessage(content=[
{"type": "text", "text": prompt},
{"type": "image", "image": f"file://{os.path.abspath(image_path)}"}
])
# 调用模型
response = llm.invoke([message])
# 提取响应文本(处理可能的列表格式)
if isinstance(response.content, str):
result_text = response.content
elif isinstance(response.content, list):
# 如果是列表,提取第一个文本元素
result_text = ""
for item in response.content:
if isinstance(item, str):
result_text = item
break
elif isinstance(item, dict) and "text" in item:
result_text = item["text"]
break
else:
result_text = str(response.content)
# 三级容错解析 JSON
result_data = parse_json_with_fallback(result_text)
if not result_data:
raise ValueError(f"JSON 解析失败: {result_text[:200]}")
# 确保必要字段
result_data.setdefault("kind", kind)
result_data.setdefault("topic", "未知主题")
result_data.setdefault("summary", "")
result_data.setdefault("entities", [])
result_data.setdefault("need_more_info", [])
return result_data
def parse_json_with_fallback(text: str) -> Dict[str, Any]:
"""
三级容错 JSON 解析
Level 1: 去除 Markdown 代码块标记
Level 2: 正则提取 JSON 片段
Level 3: 返回 None
"""
text = (text or "").strip()
if not text:
return None
# Level 1: 去除 Markdown 标记
clean = re.sub(r"^```(?:json)?\s*", "", text, flags=re.IGNORECASE)
clean = re.sub(r"\s*```$", "", clean)
try:
return json.loads(clean)
except:
pass
# Level 2: 正则提取 JSON
match = re.search(r"\{.*\}", text, flags=re.DOTALL)
if match:
try:
return json.loads(match.group(0))
except:
pass
# Level 3: 失败
return None"""
Prompt 模板:Vision Prompt + Card Prompt
"""
from typing import Dict, Any
# Vision Prompts(针对不同类型的图片)
VISION_PROMPTS = {
"chart": """你是严谨的企业识图助手。请识别这张图表,提取结构化信息。
【强制规则】
1. 只输出 JSON,不要输出任何其他文字/Markdown/解释
2. 看不清/不确定的信息,必须放入 need_more_info,绝对不要猜测
3. entities 只列出图片中明确存在的信息
【输出格式】
{
"kind": "chart",
"topic": "图表主题",
"summary": "核心数据趋势(1-2句话)",
"entities": ["关键数值1", "关键数值2", "..."],
"need_more_info": ["不确定的信息"],
"chart": {
"chart_type": "柱状图/折线图/饼图",
"trend": "上升/下降/波动",
"highlights": ["亮点1", "亮点2"]
}
}""",
"table": """你是严谨的企业识图助手。请识别这张表格,提取结构化信息。
【强制规则】
1. 只输出 JSON,不要输出任何其他文字/Markdown/解释
2. 看不清/不确定的信息,必须放入 need_more_info,绝对不要猜测
3. entities 只列出图片中明确存在的信息
【输出格式】
{
"kind": "table",
"topic": "表格主题",
"summary": "表格核心内容(1-2句话)",
"entities": ["关键字段1", "关键字段2", "..."],
"need_more_info": ["不确定的信息"],
"table": {
"headers": ["列名1", "列名2"],
"key_data": ["重要数据1", "重要数据2"]
}
}""",
"text": """你是严谨的企业识图助手。请识别这张文本图片,提取结构化信息。
【强制规则】
1. 只输出 JSON,不要输出任何其他文字/Markdown/解释
2. 看不清/不确定的信息,必须放入 need_more_info,绝对不要猜测
3. entities 只列出图片中明确存在的信息
【输出格式】
{
"kind": "text",
"topic": "文档主题",
"summary": "文档核心内容(1-2句话)",
"entities": ["关键信息1", "关键信息2", "..."],
"need_more_info": ["不确定的信息"]
}"""
}
def build_card_prompt(vision_json: Dict[str, Any], kind: str) -> str:
"""
构建学习卡片生成 Prompt
参数:
vision_json: Vision 模型识别的 JSON 数据
kind: 图片类型
返回:
str: 完整的 Prompt
"""
topic = vision_json.get("topic", "未知主题")
summary = vision_json.get("summary", "")
entities = vision_json.get("entities", [])
need_more_info = vision_json.get("need_more_info", [])
prompt = f"""你是企业知识助手。请基于以下 Vision 识别结果,生成一个结构化的学习卡片(Markdown 格式)。
【Vision 识别结果】
- 主题:{topic}
- 摘要:{summary}
- 关键实体:{', '.join(entities)}
- 待确认信息:{', '.join(need_more_info) if need_more_info else '无'}
【输出要求】
请严格按照以下 Markdown 结构输出:
# {topic}
## 📌 关键结论
{summary}
## 🔑 关键实体
{chr(10).join(f"- {entity}" for entity in entities) if entities else "暂无"}
## 💡 通俗解释
(用 1-2 句话解释这个内容的意义)
## ✏️ 练习题
1. (根据内容设计一个问题)
答案:(答案)
{"## ⚠️ 待确认信息" + chr(10) + chr(10).join(f"- {item}" for item in need_more_info) if need_more_info else ""}
【注意】
- 只输出 Markdown,不要输出其他解释
- 通俗解释要简洁易懂
- 练习题要有针对性
"""
return promptstreamlit==1.31.0
dashscope==1.14.1
langchain==0.1.9
langchain-community==0.0.24
pillow==10.2.0
pandas==2.2.0
openpyxl==3.1.2
python-dotenv==1.0.1面试中,面试官最关注的不是"你做了什么",而是"你解决了什么难题、为什么这样设计"。 以下是本项目中最值得深挖的 6 大亮点,每一个都能在面试中展开讲 3-5 分钟。
以下是根据本项目整理的简历项目经历,按照大厂简历标准格式编写,突出技术深度和业务价值。 可直接复制到简历中,根据个人情况微调即可。