news 2026/8/13 11:32:19

LangChain智能体系统化实战:从组件到生产级AI Agent的工程指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LangChain智能体系统化实战:从组件到生产级AI Agent的工程指南

1. 项目概述:从组件到系统的跨越

上次我们聊了聊Langchain的基础概念和几个核心组件的初步用法,算是给AI Agent的开发开了个头。但说实话,那只是“搭积木”的阶段,离真正做出一个能稳定运行、解决实际问题的智能体,还差得远。很多朋友照着教程跑通了“Hello World”,但一到自己的业务场景就卡壳:链条(Chain)一长就报错,记忆(Memory)时灵时不灵,工具(Tool)调用总出岔子,更别提什么复杂的智能路由和状态管理了。这感觉就像拿到了乐高零件,却不知道如何拼出一艘能下水的船。

这篇指南,我们就来啃硬骨头,聚焦于如何将分散的Langchain组件系统化地落地。我不会再重复那些基础的LLMChain调用,而是直接切入大家最常遇到的几个实战场景:如何设计一个健壮的多步骤任务处理流程?如何让Agent拥有稳定且持久的“记忆力”?如何集成外部工具并优雅地处理错误?以及,如何为整个系统构建一个可观测、可调试的“驾驶舱”?我的目标很明确:让你手里的Langchain从一套“玩具组件”升级为可以支撑真实业务逻辑的“工程框架”。无论你是想做一个智能客服、一个自动数据分析助手,还是一个复杂的决策支持系统,这里面的思路和坑点,都是相通的。

2. 核心架构设计:构建稳健的智能体工作流

当我们谈论AI Agent时,最容易陷入的误区就是“一切交给LLM”。然而,一个可靠的Agent,其核心恰恰在于对LLM能力的约束与引导。Langchain提供的各种组件,就是用来构建这种约束和引导的脚手架。一个好的架构设计,决定了Agent是“智能的助手”还是“胡言乱语的疯子”。

2.1 任务分解与链条(Chain)的进阶设计

基础的SequentialChain(顺序链)只能解决线性问题。现实中,任务往往是树状或图状的。这时,我们需要更强大的武器:LLMRouterChainMultiRouteChain

设想一个场景:用户输入“分析一下我上周的销售数据,并总结成一份报告,用邮件发给我”。这个任务至少包含三个子任务:1)获取并分析数据;2)生成文本报告;3)发送邮件。这三个任务并非总是顺序执行,比如获取数据可能失败,需要反馈给用户,而不是继续生成报告。

实战方案:使用RouterChain进行智能任务分发

我们可以设计一个路由链,先让LLM判断用户意图属于哪个类别,再将其分发到不同的处理子链。

from langchain.chains.router import MultiRouteChain, LLMRouterChain from langchain.chains.router.llm_router import RouterOutputParser from langchain.prompts import PromptTemplate from langchain.chat_models import ChatOpenAI # 1. 定义不同目的地的处理链(这里用简单链示意) sales_analysis_chain = LLMChain(llm=llm, prompt=PromptTemplate(...)) report_generation_chain = LLMChain(llm=llm, prompt=PromptTemplate(...)) email_sending_chain = LLMChain(llm=llm, prompt=PromptTemplate(...)) # 2. 定义路由提示词 route_prompt = PromptTemplate( template="""给定用户输入:{input} 请将其分类到以下一个且仅一个类别中: - 销售分析:如果用户询问销售数据、业绩、图表等。 - 报告生成:如果用户要求总结、生成文档、创建报告。 - 邮件发送:如果用户明确要求发送邮件。 - 其他:如果以上都不符合。 只返回类别名称。""", input_variables=["input"] ) # 3. 构建路由链 router_chain = LLMRouterChain.from_llm( llm=llm, prompt=route_prompt, output_parser=RouterOutputParser() ) # 4. 构建目的地链的映射 destination_chains = { “销售分析”: sales_analysis_chain, “报告生成”: report_generation_chain, “邮件发送”: email_sending_chain, } default_chain = LLMChain(llm=llm, prompt=PromptTemplate(template=“抱歉,我暂时无法处理这个请求。{input}”, input_variables=[“input”])) # 5. 组合成多路由链 multi_route_chain = MultiRouteChain( router_chain=router_chain, destination_chains=destination_chains, default_chain=default_chain, )

注意:路由的准确性完全依赖于提示词(Prompt)的设计和LLM的理解能力。务必在提示词中给出清晰、互斥的类别定义,并要求LLM只返回类别名。一个常见的坑是LLM可能会返回一句完整的话,导致后续匹配失败。因此,RouterOutputParser和严格的提示词约束至关重要。

2.2 记忆(Memory)系统的工程化实践

记忆是Agent体现“智能”和“连续性”的关键。Langchain提供了多种Memory,但直接使用常常会遇到问题,比如记忆长度爆炸、关键信息丢失、或不同会话记忆混淆。

场景深化:为客服Agent设计分层记忆系统

一个客服Agent需要记住:1)当前会话的历史(短期记忆);2)用户的个人信息和偏好(长期记忆);3)一些全局知识如产品目录(只读记忆)。

from langchain.memory import ConversationBufferWindowMemory, CombinedMemory, VectorStoreRetrieverMemory from langchain.vectorstores import Chroma from langchain.embeddings import OpenAIEmbeddings # 1. 短期记忆:只保留最近5轮对话,防止上下文过长。 short_term_memory = ConversationBufferWindowMemory(k=5, memory_key=“short_term”, input_key=“human_input”) # 2. 长期记忆:使用向量数据库存储用户档案,按需检索。 # 假设我们有一个存储用户信息的向量库 embeddings = OpenAIEmbeddings() vectorstore = Chroma(embedding_function=embeddings, persist_directory=“./user_memory_db”) retriever = vectorstore.as_retriever(search_kwargs={“k”: 2}) long_term_memory = VectorStoreRetrieverMemory(retriever=retriever, memory_key=“long_term”) # 3. 组合记忆 combined_memory = CombinedMemory(memories=[short_term_memory, long_term_memory]) # 在链中使用时,Prompt需要设计好如何利用这些记忆 agent_prompt = PromptTemplate( input_variables=[“short_term”, “long_term”, “human_input”], template=“““ 以下是本次对话的近期记录: {short_term} 以下是关于该用户的背景信息: {long_term} 用户最新消息:{human_input} 请根据以上信息进行回复。 ””” )

实操心得VectorStoreRetrieverMemory非常强大,但它不是魔法。存储的内容需要是结构化的关键信息摘要(例如:“用户张三,偏好高端产品,曾投诉过物流问题”),而不是原始的对话流水账。在用户每次对话后,可以用另一个LLM链来总结本轮交互的“有价值信息点”,然后存入向量库。这样检索效率和质量会高很多。

2.3 工具(Tool)的封装与安全调用

让Agent能调用外部工具(API、数据库、函数)是其落地价值倍增的关键。但直接暴露工具给LLM存在风险(无限循环、危险操作、资源消耗)。

安全封装与验证策略

假设我们有一个“发送邮件”的工具。

from langchain.tools import BaseTool from pydantic import BaseModel, Field from typing import Type, Optional import smtplib from email.mime.text import MIMEText class EmailInput(BaseModel): recipient: str = Field(..., description=“收件人邮箱地址”) subject: str = Field(..., description=“邮件主题”) body: str = Field(..., description=“邮件正文内容”) class SafeEmailTool(BaseTool): name = “send_email” description = “向指定的收件人发送一封电子邮件。使用时必须明确提供收件人、主题和正文。” args_schema: Type[BaseModel] = EmailInput max_calls_per_session: int = 3 # 限制单会话最大调用次数 _call_count: int = 0 def _run(self, recipient: str, subject: str, body: str) -> str: # 1. 调用次数检查 self._call_count += 1 if self._call_count > self.max_calls_per_session: return “错误:本会话内发送邮件次数已达上限。” # 2. 简单的输入验证(实际应更严格) if “@” not in recipient: return “错误:收件人邮箱地址格式无效。” # 3. 模拟发送(实际项目替换为真实SMTP调用) try: # msg = MIMEText(body) # msg[‘Subject’] = subject # msg[‘From’] = ‘agent@company.com’ # msg[‘To’] = recipient # ... 发送逻辑 print(f“[模拟] 邮件已发送至 {recipient},主题:{subject}”) return f“邮件已成功发送给 {recipient}。” except Exception as e: return f“发送邮件时出错:{str(e)}” def _arun(self, recipient: str, subject: str, body: str): raise NotImplementedError(“此工具不支持异步调用”)

工具描述(Description)是灵魂:LLM完全依靠工具的description字段来决定是否以及如何调用它。描述必须精确、无歧义、并说明使用约束。例如,“发送邮件”就比“处理邮件”好,“必须提供收件人邮箱”就明确了参数要求。

3. 状态管理与智能体(Agent)的持久化

一个复杂的Agent任务可能耗时很长,中间需要暂停、继续,甚至可能失败重启。这就需要状态管理。

3.1 使用LangGraph构建有状态的工作流

Langchain的新模块LangGraph,是构建复杂、有状态、可循环Agent的利器。它用图(Graph)的概念来定义工作流,节点是处理步骤,边是流转条件。

案例:构建一个带审核循环的文档处理Agent

工作流:生成报告 -> 自动检查报告质量 -> 如果质量不合格,则返回修改 -> 直到合格或超限。

from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated import operator # 1. 定义状态结构 class AgentState(TypedDict): task: str # 原始任务 draft: str # 报告草稿 review_comments: list[str] # 审核意见 iteration: int # 循环次数 status: Annotated[str, operator.add] # 状态:”editing”, “reviewing”, “approved”, “rejected” # 2. 定义各个节点函数 def generate_draft(state: AgentState): # 调用LLM生成初稿 prompt = f“根据以下任务生成报告草稿:{state[‘task’]}。已有的草稿或意见:{state.get(‘draft’, ‘无’)}” draft = llm.invoke(prompt) return {“draft”: draft, “status”: “reviewing”} def review_draft(state: AgentState): # 调用LLM或规则审核草稿 prompt = f“审核此报告草稿:{state[‘draft’]}。给出是否通过及修改意见。” review_result = llm.invoke(prompt) # 解析result,假设返回”APPROVED”或”NEEDS_IMPROVEMENT: 意见内容” if “APPROVED” in review_result: return {“status”: “approved”, “review_comments”: []} else: comment = review_result.split(“NEEDS_IMPROVEMENT:”)[-1].strip() return {“status”: “editing”, “review_comments”: [comment]} def finalize(state: AgentState): # 最终处理 return {“status”: “finalized”, “final_draft”: state[‘draft’]} # 3. 构建图 workflow = StateGraph(AgentState) workflow.add_node(“generate”, generate_draft) workflow.add_node(“review”, review_draft) workflow.add_node(“finalize”, finalize) # 4. 设置边和流转条件 workflow.set_entry_point(“generate”) workflow.add_edge(“generate”, “review”) # 关键:条件边。根据review节点的输出状态决定下一步 def decide_next_step(state: AgentState): if state[‘status’] == “approved”: return “finalize” elif state[‘iteration’] >= 3: # 最多修改3次 return “finalize” # 或一个“reject”节点 else: state[‘iteration’] = state.get(‘iteration’, 0) + 1 return “generate” # 返回修改 workflow.add_conditional_edges( “review”, decide_next_step, { “finalize”: “finalize”, “generate”: “generate”, } ) workflow.add_edge(“finalize”, END) # 5. 编译并运行 app = workflow.compile() initial_state = {“task”: “分析Q3市场趋势...”, “iteration”: 0} result = app.invoke(initial_state)

这个模式非常强大,它可以清晰地描述包含循环、分支、并行等复杂逻辑的Agent工作流,并且状态在整个过程中得以保持和传递。

3.2 工作流的持久化与断点续传

对于长时间运行的任务,我们需要将LangGraph的工作流状态保存到数据库(如Redis、PostgreSQL)。LangGraph本身不提供持久化,但我们可以利用其检查点(Checkpoint)机制和外部存储来实现。

简化实现思路

  1. 在每个(或关键)节点执行后,将当前的stategraph的配置序列化(如转成JSON)。
  2. 将其与一个唯一的session_id一起存入数据库。
  3. 当需要恢复时,根据session_id取出状态,重新编译graph(或从缓存加载),然后从上次保存的节点继续执行。

这涉及到对LangGraph内部机制的更深理解,通常需要定制CheckpointSaver。对于大多数应用,一个更简单的方案是:将长任务拆分为多个子任务,每个子任务完成后将结果和进度存入数据库,由外部调度器(如Celery)来管理任务队列和重试。

4. 可观测性与调试:为Agent装上“黑匣子”

Agent在线上出问题时,如果只有“它回答错了”这个信息,调试将如同大海捞针。我们必须建立一套可观测性体系。

4.1 利用LangSmith进行全链路追踪

LangChain官方推出的LangSmith是目前最好的选择。它像APM工具一样,记录下每次LLM调用、工具调用、链执行的输入、输出、耗时、Token使用量。

核心配置与使用:

import os from langsmith import Client from langchain.callbacks.tracers import LangChainTracer os.environ[“LANGCHAIN_TRACING_V2”] = “true” os.environ[“LANGCHAIN_ENDPOINT”] = “https://api.smith.langchain.com” os.environ[“LANGCHAIN_API_KEY”] = “your-api-key” os.environ[“LANGCHAIN_PROJECT”] = “My-Agent-Production” # 设置项目名,便于区分 client = Client() tracer = LangChainTracer() # 在运行你的Chain或Agent时,传入callbacks参数 result = agent.run(“用户问题”, callbacks=[tracer])

配置好后,所有执行细节都会出现在LangSmith的仪表盘上。你可以清晰地看到:是哪个Prompt导致了糟糕的回复?是哪个工具调用超时了?整个链条的耗时瓶颈在哪里?

4.2 自定义日志与监控指标

除了LangSmith,我们还需要在应用层面记录业务日志和自定义指标。

关键监控点:

  1. 成本监控:累计Token消耗(区分输入/输出),折算成API调用费用。
  2. 性能监控:各环节的响应时间P95/P99,工具调用的成功率。
  3. 质量监控:对于分类、审核等任务,可以记录LLM输出与预期结果的对比(需要基准答案)。
  4. 异常监控:记录每次LLM调用或工具调用的异常信息,特别是速率限制(Rate Limit)错误和上下文超长错误。

可以将这些指标通过logging模块输出到ELK(Elasticsearch, Logstash, Kibana)栈,或使用Prometheus等监控系统进行采集和告警。

4.3 调试技巧:当Agent“胡言乱语”时

即使有了监控,定位具体问题也需要技巧。下面是一个排查清单:

现象可能原因排查步骤
Agent完全偏离主题系统提示词(System Prompt)太弱或被覆盖;上下文窗口混入了无关信息。1. 检查并强化系统提示词,明确角色和边界。
2. 检查Memory内容,是否引入了干扰对话历史。
3. 在LangSmith中查看最终发给LLM的完整Prompt。
工具调用错误或不被调用工具描述不清晰;Agent执行器(Executor)的Max Iterations设置过小。1. 精炼工具描述,确保LLM能理解其功能和输入格式。
2. 检查Agent的max_iterations参数,对于复杂任务需要调大。
3. 在LangSmith中查看Agent的“思考过程”(如果使用ReAct等模式)。
响应速度极慢某个工具(如网络请求、数据库查询)响应慢;LLM API本身延迟高。1. 在LangSmith中查看各步骤耗时,定位瓶颈。
2. 为工具调用设置超时(timeout)。
3. 考虑对耗时的工具进行异步调用或缓存结果。
记忆混乱不同会话的Memory未隔离;VectorStore记忆检索出无关内容。1. 确保每个用户/会话有独立的Memory实例或Session ID。
2. 调整向量记忆检索的相似度阈值和返回数量(k值)。
3. 对存入记忆的内容进行更严格的清洗和摘要。

一个黄金法则:在开发阶段,尽量让Agent的“思考过程”可视化。对于使用ReAct或类似模式的Agent,强制它输出“Thought:”, “Action:”, “Observation:”这样的中间步骤,这比直接看最终答案更能发现问题根源。

5. 性能优化与成本控制

当Agent从Demo走向生产,性能和成本立刻成为核心关切。

5.1 上下文管理:与Token消耗的战争

LLM的上下文窗口(Context Window)是宝贵的资源,也是成本的主要构成。无限制地将所有历史对话和文档塞进上下文,不仅昂贵,还会导致模型性能下降(中间遗忘问题)。

优化策略:

  1. 摘要式记忆(Summary Memory):不要存储完整的对话历史。定期(例如每5轮对话)使用一个独立的LLM调用,将之前的对话总结成一段精炼的摘要,然后用这个摘要替代原始历史,作为新的“记忆”输入下一轮。ConversationSummaryBufferMemory就是这个思路。
  2. 选择性上下文注入:对于基于检索(Retrieval)的记忆或知识库,不要一次性注入所有检索结果。可以先让LLM根据问题生成一个“搜索查询”,再用这个查询去检索最相关的几条信息注入上下文。这就是RetrievalQA链的核心思想。
  3. 流式处理与窗口滑动:对于超长文档处理,采用“Map-Reduce”或“Refine”模式。先将文档切块(Map),分别处理每个块,再汇总结果(Reduce),避免一次性传入整个文档。

5.2 缓存与异步:加速响应与节省开销

请求缓存:对于相同的LLM Prompt输入,其输出在短时间内是确定的。可以使用LangchainCache功能(支持内存、SQLite、Redis等后端)来缓存结果。

from langchain.cache import InMemoryCache from langchain.globals import set_llm_cache set_llm_cache(InMemoryCache())

这样,当完全相同的提问再次出现时,会直接返回缓存结果,极大节省成本和时间。注意,这适用于相对静态的知识问答,对于需要实时性的对话,要慎用或设置较短的过期时间。

异步调用:当Agent需要并行调用多个工具,或者同时处理多个独立的任务分支时,使用异步(Async)可以大幅减少总等待时间。确保你使用的LLM模型、工具和链都支持异步接口(通常有ainvoke,acall等方法),并在async函数中使用await

5.3 模型选型与降级策略

不是所有任务都需要GPT-4。建立一套模型降级策略:

  • 复杂推理、创意生成:使用能力最强、最贵的模型(如GPT-4)。
  • 简单分类、信息提取、格式化输出:使用性价比高的模型(如GPT-3.5-Turbo, Claude Haiku)。
  • 简单的意图识别、路由:甚至可以考虑使用更小、更快的开源模型(通过本地部署或廉价API)。

可以在你的MultiRouteChainLangGraph的判断节点,设计一个逻辑:先用一个快速廉价的模型判断任务复杂度,再决定调用哪个主力模型来处理。这本身就是一个有趣的元Agent(Meta-Agent)设计。

6. 部署与运维:让Agent稳定服务

开发调试完毕的Agent,最终需要交付给用户使用。部署不是简单的跑起一个Python脚本。

6.1 部署模式选择

  1. Web API服务(推荐):使用FastAPI或Flask将你的Agent封装成RESTful API。这是最灵活、最通用的方式,便于前端、移动端或其他服务集成。

    from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() # 假设agent是已经定义好的Langchain Agent或Chain # 注意:在生产中,agent的初始化(加载模型、数据库连接等)应在启动时完成,而不是每次请求都新建。 class QueryRequest(BaseModel): session_id: str question: str @app.post(“/chat”) async def chat(request: QueryRequest): # 根据session_id获取或创建对应的记忆体 memory = get_memory_for_session(request.session_id) # 运行Agent response = await agent.arun({“input”: request.question, “memory”: memory}) return {“response”: response}

    关键点:确保Agent实例或关键资源(如LLM客户端、数据库连接)是全局或可高效复用的,避免每次请求都重新加载模型,那将无法承受任何流量。

  2. 消息队列消费者:对于处理耗时较长、无需实时返回的任务(如报告生成、批量数据处理),可以让Agent作为Celery或RabbitMQ的消费者,从队列中获取任务,处理完成后将结果写入数据库或回调另一个服务。

6.2 配置管理与密钥安全

绝对不要将API密钥、数据库密码等硬编码在代码中。使用环境变量或专业的配置管理工具(如AWS Parameter Store, HashiCorp Vault)。

import os from langchain.chat_models import ChatOpenAI # 从环境变量读取 openai_api_key = os.environ.get(“OPENAI_API_KEY”) if not openai_api_key: raise ValueError(“请在环境变量中设置 OPENAI_API_KEY”) llm = ChatOpenAI(model=“gpt-3.5-turbo”, api_key=openai_api_key)

在Docker或Kubernetes部署时,通过Secrets来管理这些敏感信息。

6.3 健康检查、就绪探针与优雅退出

你的Agent服务需要告诉部署平台(如K8s)它是否健康。

  • 健康检查(Health Check):一个简单的/health端点,返回200状态码。可以加入对关键依赖(如向量数据库、LLM API连通性)的检查。
  • 就绪探针(Readiness Probe):在服务启动完成,所有资源(模型、数据库连接池)初始化完毕后再返回就绪。防止流量打到还未准备好的实例上。
  • 优雅退出(Graceful Shutdown):监听退出信号(如SIGTERM),在收到信号后,停止接收新请求,完成正在处理的请求,再释放资源退出。这可以通过FastAPI的@app.on_event(“shutdown”)或类似机制实现。

6.4 版本管理与回滚

Agent的核心——Prompt、工作流逻辑、工具集——会频繁迭代。必须有版本管理。

  • 代码化一切:将Prompt模板、Chain的组装逻辑都写在代码中,并使用Git进行版本控制。
  • Prompt版本化:对于重要的系统Prompt,可以将其内容存储在数据库或对象存储(如S3)中,并附带版本号。服务启动时拉取指定版本的Prompt。这样可以在不重启服务的情况下,通过修改配置来切换Prompt版本,快速进行A/B测试或回滚。
  • 模型版本化:记录每次部署所使用的LLM模型名称和版本(如gpt-4-1106-preview)。当LLM服务商更新模型时,你可以明确知道当前线上服务用的是哪个版本,评估升级风险。

走到这一步,你的AI Agent已经不再是一个实验性的脚本,而是一个有架构、可观测、可运维的生产级服务了。这个过程充满挑战,但每解决一个实际问题,你对智能体系统的理解就会加深一层。记住,最好的学习永远来自于动手去构建,然后看着它真正运行起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/13 11:31:51

华为OD机试新系统真题 【LLM推理批次最大化】

LLM推理批次最大化(C/Py/Java /Js/Go/C)题解 华为OD机试新系统真题 华为OD上机考试新系统真题 8月12号 100分题型 华为OD机试新系统真题目录点击查看: 华为OD机试新系统真题题库目录|机考题库 算法考点详解 题目内容 大语言模型推理时,显存中有一个 …

作者头像 李华
网站建设 2026/8/13 11:30:29

CATIA几何特征智能识别实战:曲面法线点阵批量化生成的完整方案

CATIA几何特征智能识别实战:曲面法线点阵批量化生成的完整方案 【免费下载链接】pycatia python module for CATIA V5 automation 项目地址: https://gitcode.com/gh_mirrors/py/pycatia 面对成百上千个需要逐一处理的曲面采样点,很多工程师的第一…

作者头像 李华
网站建设 2026/8/13 11:29:01

LinkSwift网盘直链助手:9大平台一键解析的终极解决方案

LinkSwift网盘直链助手:9大平台一键解析的终极解决方案 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 ,支持 百度网盘 / 阿里云盘 / 中国移动云盘 / 天翼…

作者头像 李华
网站建设 2026/8/13 11:27:55

java中的线程游戏的编辑1

线程游戏 线程、进程与线程游戏 在编写线程游戏之前,我们首先要明白线程是什么、进程是什么,以及进程和线程之间的关系。线程: 进程内部并发执行的执行单元,CPU 调度的最小单位。特点: 同一个进程里所有线程共享内存资…

作者头像 李华
网站建设 2026/8/13 11:21:48

U盘操作全解析:从读取到安全弹出的技术指南

1. U盘基础操作全解析:从读取到安全弹出的完整指南 U盘作为最常用的便携存储设备,几乎每天都会出现在我们的工作和生活中。但很多人可能没有意识到,简单的插入、读取和弹出操作背后,其实隐藏着一系列值得注意的技术细节和潜在风险…

作者头像 李华