08 实践课-LangChain 基础智能体原型开发

LangChain 基础智能体原型开发

关联:索引

  1. 场景提问:分拣员说“查一下玻璃制品怎么分拣”,系统怎么做到“查规则→返回标准→让结果可复验”?
sorting_agent_practice/
  app.py
  tools_sorting.py  # 两个工具都写在这个文件里(规则查询/反馈回执)
  intent.py
  prompts.py
  requirements.txt

2. 阶段式实现路线(从零到最小闭环综合项目)

最终交付目标:在 sorting_agent_practice/ 下运行 python app.py,完成“接收指令 → 意图解析 → 触发工具/Agent → 输出可复验依据”的最小闭环。

python==3.10.12
langchain==1.2.7
langchain-core==1.2.17
langchain-community==0.4.1
langgraph==1.0.10(本节不强制使用,可作为进阶编排)
dashscope==1.25.13
python-dotenv==1.2.2
fastapi[all]==0.111.0(本节不强制使用;用于后续 API 服务化;安装后通常会带上 pydantic)
uvicorn==0.23.2(本节不强制使用;用于后续 API 运行)

安装(示例,Conda + Pip,推荐):

conda create -n sorting-agent python=3.10.12 -y
conda activate sorting-agent

pip install langchain==1.2.7 langchain-core==1.2.17 langchain-community==0.4.1 langgraph==1.0.10 dashscope==1.25.13 python-dotenv==1.2.2 "fastapi[all]==0.111.0" uvicorn==0.23.2

如果你已经创建并激活了 Conda 环境:直接执行 pip install ... 这一行即可。

说明:

4. 通义千问 Key 配置(演示规范)

.env 示例:

DASHSCOPE_API_KEY=你的_key

Python 里读取 .env 的关键点:在初始化 LLM 之前调用 load_dotenv(),让环境变量进入 os.getenv()
如果你是从项目外层目录运行脚本,导致 .env 没被找到,优先检查当前工作目录是否为项目根目录。

export DASHSCOPE_API_KEY="你的_key"
$env:DASHSCOPE_API_KEY="你的_key"

setx DASHSCOPE_API_KEY "你的_key"

2. 阶段 1:示例工具 1(规则查询,最小可用)

文件:tools_sorting.py(关键结构示例)

from langchain_core.tools import tool
import uuid

_RULES = {
    "glass": {
        "rule_id": "R-GLASS-01",
        "rule_text": "玻璃制品:轻放、防碰撞;单独箱;贴易碎标;优先人工复核。",
        "action": "use_fragile_bin",
    },
    "battery": {
        "rule_id": "R-BATT-02",
        "rule_text": "电池类:绝缘包装;远离金属;单独分拣;异常鼓包立即隔离。",
        "action": "use_battery_bin",
    },
}

@tool("query_sorting_rule")
def query_sorting_rule(item_type: str) -> str:
    """查询分拣规则。输入为物品类型英文关键字,如 glass/battery。返回规则摘要。"""
    trace_id = uuid.uuid4().hex[:8]
    k = (item_type or "").strip().lower()
    if not k:
        return f"ERROR: item_type is empty | trace_id={trace_id}"
    rule = _RULES.get(k)
    if not rule:
        return f"NOT_FOUND: no rule for item_type={k} | trace_id={trace_id}"
    return f"{rule['rule_id']} | {rule['rule_text']} | action={rule['action']} | trace_id={trace_id}"

3. 阶段 1:示例工具 2(结果反馈,用于记录异常/结果)

文件:tools_sorting.py(同一文件,继续追加第二个工具)

from langchain_core.tools import tool
import uuid

@tool("submit_sorting_feedback")
def submit_sorting_feedback(task_id: str, ok: bool, message: str) -> str:
    """提交分拣反馈(演示版:返回可验收的回执字符串)。"""
    trace_id = uuid.uuid4().hex[:8]
    task_id = (task_id or "").strip()
    if not task_id:
        return f"ERROR: task_id is empty | trace_id={trace_id}"
    msg = (message or "").strip()
    if not msg:
        return f"ERROR: message is empty | trace_id={trace_id}"
    status = "OK" if ok else "FAIL"
    return f"RECEIPT: task_id={task_id} status={status} message={msg} | trace_id={trace_id}"

文件:prompts.py(示例)

SYSTEM_PROMPT_TEXT = (
    "你是分拣车间的智能助手。优先使用工具获取规则或生成回执。回答必须包含可复验依据(如规则编号/回执/trace_id)。"
    "不要输出或猜测任何密钥。遇到信息不足先追问澄清,不要编造规则。"
)

2. LangChain Agent 最小闭环(示例)

文件:app.py(示例结构)

阶段 3 目标:先不做路由与意图识别,仅跑通 “LLM + Tool + Prompt + Agent(create_agent)” 的最小闭环。

import os
from dotenv import load_dotenv
from langchain_core.messages import HumanMessage
try:
    from langchain_community.chat_models import ChatTongyi
except ImportError:
    from langchain_community.chat_models.tongyi import ChatTongyi

from prompts import SYSTEM_PROMPT_TEXT
from tools_sorting import query_sorting_rule, submit_sorting_feedback

load_dotenv()

def build_llm():
    if not os.getenv("DASHSCOPE_API_KEY"):
        raise RuntimeError("DASHSCOPE_API_KEY is not set")
    return ChatTongyi(model="qwen-plus", temperature=0)

def main():
    llm = build_llm()
    tools = [query_sorting_rule, submit_sorting_feedback]

    user_input = "查一下 glass 的分拣规则"
    from langchain.agents import create_agent
    agent = create_agent(model=llm, tools=tools, system_prompt=SYSTEM_PROMPT_TEXT, debug=True)
    out = agent.invoke({"messages": [HumanMessage(content=user_input)]})
    messages = out.get("messages") or []
    final_message = messages[-1] if messages else None
    print(getattr(final_message, "content", out))

if __name__ == "__main__":
    main()

  1. 快问快答:为什么要写意图识别?(答案要包含:稳定性、可控性、可测试性)

2. 指令解析与意图匹配(规则优先、LLM 回退)

企业级主流实现(本采用,替换正则示例):

文件:intent.py(示例)

import re
import json
import uuid
from dataclasses import dataclass
from typing import Literal, Optional

from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field, ValidationError

class IntentPayload(BaseModel):
    intent: Literal["QUERY_RULE", "SUBMIT_FEEDBACK", "GENERAL"]
    item_type: Optional[str] = None
    task_id: Optional[str] = None
    ok: Optional[bool] = None
    message: Optional[str] = None
    confidence: float = Field(default=0.0, ge=0.0, le=1.0)
    need_clarification: bool = False
    clarification_question: Optional[str] = None

@dataclass
class IntentResult:
    trace_id: str
    intent: str
    item_type: Optional[str] = None
    task_id: Optional[str] = None
    ok: Optional[bool] = None
    message: Optional[str] = None
    confidence: float = 0.0
    need_clarification: bool = False
    clarification_question: Optional[str] = None
    no_tools: bool = False

def detect_intent(text: str, llm) -> IntentResult:
    trace_id = uuid.uuid4().hex[:8]
    t = (text or "").strip()
    if not t:
        return IntentResult(
            trace_id=trace_id,
            intent="GENERAL",
            confidence=0.0,
            need_clarification=True,
            clarification_question="你想查询分拣规则,还是提交分拣反馈?请补充关键信息。",
        )

    explanation_keywords = (
        "为什么",
        "为何",
        "原理",
        "原因",
        "区别",
        "差别",
        "对比",
        "比较",
    )
    if any(k in t for k in explanation_keywords):
        return IntentResult(trace_id=trace_id, intent="GENERAL", confidence=1.0, no_tools=True)

    tokens = t.split()
    if len(tokens) >= 2 and tokens[0].upper() == "RULE":
        item_type = tokens[1].strip().lower()
        if not item_type:
            return IntentResult(
                trace_id=trace_id,
                intent="QUERY_RULE",
                confidence=1.0,
                need_clarification=True,
                clarification_question="请提供物品类型关键字(如 glass/battery)。",
            )
        return IntentResult(trace_id=trace_id, intent="QUERY_RULE", item_type=item_type, confidence=1.0)

    if len(tokens) >= 4 and tokens[0].upper() == "FEEDBACK":
        ok = tokens[2].upper() in {"OK", "SUCCESS", "TRUE"}
        task_id = tokens[1].strip()
        message = " ".join(tokens[3:]).strip()
        if not task_id or not message:
            return IntentResult(
                trace_id=trace_id,
                intent="SUBMIT_FEEDBACK",
                confidence=1.0,
                need_clarification=True,
                clarification_question="请补充 task_id 与反馈内容(例如:FEEDBACK t001 FAIL 玻璃破损)。",
            )
        return IntentResult(trace_id=trace_id, intent="SUBMIT_FEEDBACK", task_id=task_id, ok=ok, message=message, confidence=1.0)

    if ("规则" in t or "分拣规则" in t or "怎么分拣" in t or "如何分拣" in t or "怎么处理" in t):
        m = re.search(r"[A-Za-z][A-Za-z0-9_-]*", t)
        if not m:
            return IntentResult(
                trace_id=trace_id,
                intent="QUERY_RULE",
                confidence=0.9,
                need_clarification=True,
                clarification_question="你要查询哪一类物品的规则?请给出关键字(如 glass/battery)。",
            )
        return IntentResult(
            trace_id=trace_id,
            intent="QUERY_RULE",
            item_type=m.group(0).lower(),
            confidence=0.9,
        )

    if t.startswith("提交反馈") or t.startswith("反馈") or "提交反馈" in t or "反馈" in t:
        task_id_match = re.search(r"\b[tT]\d+\b", t)
        task_id = task_id_match.group(0) if task_id_match else None
        ok: Optional[bool]
        if any(x in t for x in ["失败", "异常", "破损", "报错", "错"]):
            ok = False
        elif any(x in t for x in ["成功", "正常", "OK", "ok"]):
            ok = True
        else:
            ok = None
        msg = t
        msg = msg.replace("提交反馈", "").replace("反馈", "").strip()
        if task_id:
            msg = msg.replace(task_id, "").strip()
        msg = msg.strip(" ,,::")
        if not task_id:
            return IntentResult(
                trace_id=trace_id,
                intent="SUBMIT_FEEDBACK",
                confidence=0.9,
                need_clarification=True,
                clarification_question="请补充 task_id(例如:提交反馈 t001 失败,玻璃有破损)。",
            )
        if ok is None:
            return IntentResult(
                trace_id=trace_id,
                intent="SUBMIT_FEEDBACK",
                task_id=task_id,
                confidence=0.9,
                need_clarification=True,
                clarification_question="本次分拣结果是成功还是失败?请补充“成功/失败”。",
            )
        if not msg:
            return IntentResult(
                trace_id=trace_id,
                intent="SUBMIT_FEEDBACK",
                task_id=task_id,
                ok=ok,
                confidence=0.9,
                need_clarification=True,
                clarification_question="请补充反馈内容(例如:玻璃破损/电池鼓包)。",
            )
        return IntentResult(
            trace_id=trace_id,
            intent="SUBMIT_FEEDBACK",
            task_id=task_id,
            ok=ok,
            message=msg,
            confidence=0.9,
        )

    prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "你是企业系统里的指令解析器。把用户输入解析成严格 JSON(只输出 JSON,不要多余文字)。\n"
                "JSON 字段:intent(QUERY_RULE/SUBMIT_FEEDBACK/GENERAL), item_type, task_id, ok, message, confidence(0-1), need_clarification(true/false), clarification_question。\n"
                "规则:\n"
                "1) 缺少关键槽位时,need_clarification=true,并给出 clarification_question。\n"
                "2) 不确定时 intent=GENERAL,confidence 低。\n"
                "3) 不要编造 task_id 或 item_type。",
            ),
            ("human", "{text}"),
        ]
    )
    raw = llm.invoke(prompt.format_messages(text=t)).content
    start = raw.find("{")
    end = raw.rfind("}")
    if start < 0 or end < 0 or end <= start:
        return IntentResult(trace_id=trace_id, intent="GENERAL", confidence=0.0)

    try:
        data = json.loads(raw[start : end + 1])
        if hasattr(IntentPayload, "model_validate"):
            payload = IntentPayload.model_validate(data)
        else:
            payload = IntentPayload.parse_obj(data)
    except (json.JSONDecodeError, ValidationError):
        return IntentResult(trace_id=trace_id, intent="GENERAL", confidence=0.0)

    return IntentResult(
        trace_id=trace_id,
        intent=payload.intent,
        item_type=payload.item_type,
        task_id=payload.task_id,
        ok=payload.ok,
        message=payload.message,
        confidence=payload.confidence,
        need_clarification=payload.need_clarification,
        clarification_question=payload.clarification_question,
    )

企业级补充要求(内已在代码示例体现):

1. 路由统一口径

from intent import detect_intent
from langchain_core.messages import HumanMessage, SystemMessage
from prompts import SYSTEM_PROMPT_TEXT
from tools_sorting import query_sorting_rule, submit_sorting_feedback

def run_with_router(agent, llm, user_input: str) -> str:
    ir = detect_intent(user_input, llm)

    if ir.need_clarification:
        q = ir.clarification_question or "请补充关键信息后再试一次。"
        return f"【需要澄清】{q} | parse_trace_id={ir.trace_id}"

    if ir.intent == "QUERY_RULE":
        tool_out = query_sorting_rule.invoke({"item_type": ir.item_type})
        return f"【规则查询结果】{tool_out} | parse_trace_id={ir.trace_id}"

    if ir.intent == "SUBMIT_FEEDBACK":
        tool_out = submit_sorting_feedback.invoke(
            {"task_id": ir.task_id, "ok": ir.ok, "message": ir.message}
        )
        return f"【反馈回执】{tool_out} | parse_trace_id={ir.trace_id}"

    if ir.intent == "GENERAL" and getattr(ir, "no_tools", False):
        msg = llm.invoke([SystemMessage(content=SYSTEM_PROMPT_TEXT), HumanMessage(content=user_input)])
        content = getattr(msg, "content", str(msg))
        return f"【解释】{content} | no_tools=true | parse_trace_id={ir.trace_id}"

    out = agent.invoke({"messages": [HumanMessage(content=user_input)]})
    messages = out.get("messages") or []
    tool_called = any(getattr(m, "type", None) == "tool" for m in messages)
    final_message = messages[-1] if messages else None
    content = getattr(final_message, "content", str(out))
    if tool_called:
        return f"{content} | parse_trace_id={ir.trace_id}"
    return f"{content} | no_tool_called=true | parse_trace_id={ir.trace_id}"
import os
from dotenv import load_dotenv
from langchain_core.messages import HumanMessage, SystemMessage
try:
    from langchain_community.chat_models import ChatTongyi
except ImportError:
    from langchain_community.chat_models.tongyi import ChatTongyi

from intent import detect_intent
from prompts import SYSTEM_PROMPT_TEXT
from tools_sorting import query_sorting_rule, submit_sorting_feedback

load_dotenv()

def build_llm():
    if not os.getenv("DASHSCOPE_API_KEY"):
        raise RuntimeError("DASHSCOPE_API_KEY is not set")
    return ChatTongyi(model="qwen-plus", temperature=0)

def build_agent(llm):
    tools = [query_sorting_rule, submit_sorting_feedback]
    from langchain.agents import create_agent
    return create_agent(model=llm, tools=tools, system_prompt=SYSTEM_PROMPT_TEXT, debug=True)

def run_with_router(agent, llm, user_input: str) -> str:
    ir = detect_intent(user_input, llm)

    if ir.need_clarification:
        q = ir.clarification_question or "请补充关键信息后再试一次。"
        return f"【需要澄清】{q} | parse_trace_id={ir.trace_id}"

    if ir.intent == "QUERY_RULE":
        tool_out = query_sorting_rule.invoke({"item_type": ir.item_type})
        return f"【规则查询结果】{tool_out} | parse_trace_id={ir.trace_id}"

    if ir.intent == "SUBMIT_FEEDBACK":
        tool_out = submit_sorting_feedback.invoke(
            {"task_id": ir.task_id, "ok": ir.ok, "message": ir.message}
        )
        return f"【反馈回执】{tool_out} | parse_trace_id={ir.trace_id}"

    if ir.intent == "GENERAL" and getattr(ir, "no_tools", False):
        msg = llm.invoke([SystemMessage(content=SYSTEM_PROMPT_TEXT), HumanMessage(content=user_input)])
        content = getattr(msg, "content", str(msg))
        return f"【解释】{content} | no_tools=true | parse_trace_id={ir.trace_id}"

    out = agent.invoke({"messages": [HumanMessage(content=user_input)]})
    messages = out.get("messages") or []
    tool_called = any(getattr(m, "type", None) == "tool" for m in messages)
    final_message = messages[-1] if messages else None
    content = getattr(final_message, "content", str(out))
    if tool_called:
        return f"{content} | parse_trace_id={ir.trace_id}"
    return f"{content} | no_tool_called=true | parse_trace_id={ir.trace_id}"

def main():
    llm = build_llm()
    agent = build_agent(llm)

    test_inputs = [
        "查一下 glass 的分拣规则",
        "提交反馈 t001 失败,玻璃有破损",
        "为什么电池要单独分拣?",
        "",
    ]
    for s in test_inputs:
        print("USER:", s)
        print("ASSISTANT:", run_with_router(agent, llm, s))
        print("-" * 60)

if __name__ == "__main__":
    main()

  1. 查询规则:
  1. 提交反馈:
  1. 普通咨询:

1. Key / 权限问题

  1. 检查环境变量是否生效(新终端是否加载)
  2. 检查 Key 是否含空格/引号错误

2. 依赖与版本问题

  1. pip show langchain langchain-community langchain-core 查看版本
  2. 确认是否安装了 langchain-community
  3. 统一在虚拟环境中安装,避免系统 Python 污染

3. 工具不触发 / 输出不可复验

  1. 路由是否命中(打印/记录意图识别结果)
  2. 工具输入是否为空或不合法(工具返回 ERROR/NOT_FOUND)
  3. Prompt 是否要求“必须引用工具结果”(System 约束是否足够明确)

提示词模板(可直接复制):

你是 LangChain 智能体开发专家。请基于“仓储分拣指令处理”场景,输出一个 最小可运行 的智能体原型项目(可在 Windows/conda 环境运行)。请严格按要求生成内容,避免无关扩展。

一、目标

- 产出一个可运行 Demo:用户输入一句话,系统先做意图识别(规则匹配),再决定是否调用工具;解释类问题走 GENERAL 直接解释,不触发工具。
- 使用 LangChain 新版 API : from langchain.agents import create_agent ,不得使用 AgentExecutor / create_tool_calling_agent 。
二、组件要求(必须全部满足)

1. 初始化通义千问 LLM
- 使用 langchain_community.chat_models.ChatTongyi
- Key 通过环境变量 DASHSCOPE_API_KEY 读取(可用 python-dotenv 加载 .env )
- 代码中严禁写死 API Key
2. 定义 2 个工具(Tool)
- query_sorting_rule(item_type: str) -> str :返回包含 rule_id 与 trace_id 的可复验字符串
- submit_sorting_feedback(task_id: str, ok: bool, message: str) -> str :返回包含回执字段与 trace_id 的可复验字符串
- Tool 输出必须包含可复验依据:规则编号或回执 + trace_id
3. 定义 system_prompt
- 约束:优先使用工具获取规则/生成回执;回答必须包含可复验依据;信息不足先追问澄清;不要输出或猜测任何密钥
- system_prompt 统一放在 prompts.py (例如 SYSTEM_PROMPT_TEXT ),应用端复用该变量
4. 用 create_agent 组装并调用
- Agent 用 system_prompt=SYSTEM_PROMPT_TEXT
- 演示调用: agent.invoke({"messages": [HumanMessage(content=user_input)]})
三、意图识别模块(必须给出可测试的规则匹配实现)

- 单独文件 intent.py ,提供 detect_intent(text: str, llm) -> IntentResult
- 意图枚举: QUERY_RULE / SUBMIT_FEEDBACK / GENERAL
- 规则优先级(从高到低):
  1. 空输入:需要澄清(need_clarification=true,给 clarification_question)
  2. 解释类问题:只要输入 以/包含 “为什么/原理/原因/区别/差别/对比/比较/为何”等,直接 GENERAL ,并 显式标记 no_tools=true (确保不会触发工具)
  3. 显式规则查询:包含“规则/分拣规则/怎么分拣/如何分拣/怎么处理”等,提取 item_type(英文关键字优先),否则需要澄清
  4. 反馈类:包含“提交反馈/反馈”等,抽取 task_id(如 t001),推断 ok(成功/失败),缺槽位则需要澄清
  5. 其余情况:GENERAL(可选:允许用 LLM 输出 JSON 兜底,但必须严格 JSON、异常回退 GENERAL)
四、可测试用例(必须提供 3 条 + 期望结果) 请给出 3 条输入与期望的 intent 路由结果(写成表格或列表),至少包含:

- 规则查询例(应走 QUERY_RULE 并调用 query_sorting_rule)
- 反馈提交例(应走 SUBMIT_FEEDBACK 并调用 submit_sorting_feedback)
- 解释类例(例如“为什么电池要单独分拣?”应走 GENERAL 且 no_tools=true,不调用工具)
五、约束与错误处理(必须体现到代码与说明中)

- 工具输入输出可复验(规则编号/回执 + trace_id)
- 异常必须返回明确错误信息(例如未配置 Key、依赖缺失、网络失败、解析失败)
- 不得输出任何 API Key 或提示用户粘贴 Key 到代码
- 输出必须是可复制运行的完整代码(不要省略关键导入与依赖)
六、排错清单(必须给 4 个常见报错,格式固定) 按以下格式给出 4 个常见报错的排查:

- 现象 → 原因 → 定位 → 修复 → 复验
   必须覆盖:
1. Key 未配置/读取不到
2. 网络/限流/服务不可用
3. 依赖缺失或版本不匹配(尤其 LangChain 新旧 API 导入错误)
4. 工具不触发(意图识别误判、解释类问题误走工具、路由逻辑错误)
七、输出格式(严格按顺序输出,不要夹杂多余解释)

1. 推荐目录结构(树形)
2. requirements.txt(完整内容)
3. 关键代码文件内容(逐个文件给出完整内容,至少包括):
   - app.py (最小运行 demo:直接走 create_agent + tool)
   - app_with_intent.py (带意图路由:QUERY_RULE/SUBMIT_FEEDBACK/GENERAL + no_tools 分支)
   - tools_sorting.py
   - intent.py
   - prompts.py
4. 3 条测试用例与期望结果
5. 排错清单(4 条,“现象→原因→定位→修复→复验”)

八、综合项目(最终最小闭环,对照拼装)

运行方式(项目根目录下):

python app.py

作业:布置

1)提交智能体基础框架代码(含 LLM 配置、Tool 开发)
2)提交意图识别逻辑代码及测试截图(至少 3 个测试用例)
3)提交 AI 辅助生成 / 优化智能体代码的交互记录及优化后的代码