【开发】Agent开发基础

【开发】Agent开发基础

基本信息

API Key及管理

对于不同的大模型,需要通过不同平台的API进行调用,其中需要申请API Key

API Key由于直接对应了访问的权限,所以在代码中不能硬编码,而且不同管理员可以通过自己的API Key去进行访问,所以最佳的选项是配置到环境变量

Windows环境就是直接在环境变量处设置就行了,比如阿里百炼平台的API Key在配置完之后就可以用如下方法取用:

api_key = os.getenv("DASHSCOPE_API_KEY")

涉及工具

  • 模型调用:openai接口是大多数模型都会兼容的
  • AI工作流:Langchain/Langgraph(本文主要是langchain)
  • 文本切割/向量化:可以用langchain_community中的相关组件

OpenAI使用:最底层接口

基本操作

导包之后首先要创建一个客户端对象

client = OpenAI(
    api_key = os.getenv("DASHSCOPE_API_KEY"),
    base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
)

接着通过client注入对话信息:

response = client.chat.completions.create(
    model = "qwen3-max",
    messages = [
        {"role": "system", "content": "你是一个Python编程专家, 废话不多简单回答"},
        {"role": "assistant", "content": "好的,我是变成专家,并且废话不多, 你要问什么?"},
        {"role": "user", "content": "输出1-10的数字,使用Python代码"}
    ]
)

messages是一个列表,其中包含三种角色信息:

  • system:里面书写对于Agent的对话基本要求
  • assistant:主体是模型,用于模拟对话信息,可以让模型自主参照
  • user:用户信息

流式输入

在前面的基础上,create函数先加上stream = True

这时候返回的内容就是一段一段的,不能直接print,需要通过循环给出

for chunk in response:
    print(chunk.choices[0].delta.content, end = "", flush = True)

历史信息

在create函数中,通过加入user和assistant的模拟对话,可以让模型得知历史信息

response = client.chat.completions.create(
    model = "qwen3-max",
    messages = [
        {"role": "system", "content": "你是一个AI助理, 回答简洁"},
        {"role": "user", "content": "小明有2条宠物狗"},
        {"role": "assistant", "content": "好的"},
        {"role": "user", "content": "小红有3只宠物猫"},
        {"role": "assistant", "content": "好的"},
        {"role": "user", "content": "总共有几只宠物?"},
    ],
    stream = True
)

for chunk in response:
    print(chunk.choices[0].delta.content, end = "", flush = True)

提示词工程:Few-Shot

Few-Shot相对于Zero-Shot,指的是给少量参照示例,让模型得知工作内容和输出格式等信息,更容易对齐需求

本节下将给出三个例子

文本分类

在历史信息中喂入模拟的对话过程,让模型理解要分类的内容

examples_data = {
    "新闻报道": "今日,国家统计局发布数据显示,我国制造业PMI指数连续三个月保持在扩张区间,显示经济运行总体平稳。多位专家表示,宏观政策正在逐步显效。",
    "财务报告": "公司发布2025年第一季度财报,营业收入同比增长18%,净利润增长12%。受益于成本控制和主营业务扩张,公司整体盈利能力持续提升。",
    "公司公告": "本公司董事会宣布,已完成对某科技公司的股权收购。本次交易将进一步增强公司在人工智能领域的技术储备和市场竞争力。",
    "分析师报告": "多家机构分析认为,新能源行业未来三年仍将保持高速增长。随着政策支持力度加大,相关企业盈利能力有望进一步提升。"
}

examples_types = ["新闻报道", "财务报告", "公司公告", "分析师报告"]

questions = [
    "国家统计局今日发布数据,显示消费市场持续回暖,居民消费信心明显提升。",
    "公司2025年上半年实现净利润5亿元,同比增长25%,经营现金流显著改善。",
    "某上市公司公告称,将以现金方式收购一家半导体企业的控股权。",
    "券商研究指出,人工智能产业链正进入快速发展阶段,相关公司值得重点关注。",
    "公司表示将调整未来发展战略,加大研发投入,以应对行业竞争加剧。",
    "某企业宣布拟发行债券用于补充流动资金,优化资本结构。",
    "小明说今天天气真不错。"
]

messages = [
    {"role": "system",
     "content": "你是一名金融专家,将文本分类为[\"新闻报道\", \"财务报告\", \"公司公告\", \"分析师报告\"],不清楚的分类为\"不清楚类别\""}
]

for key, value in examples_data.items():
    messages.append({"role": "user", "content": value})
    messages.append({"role": "assistant", "content": key})

for q in questions:
    response = client.chat.completions.create(
        model = "qwen3-max",
        messages = messages + [{"role": "user", "content": f"按照示例,回答这段文本的分类类别:{q}"}]
    )

    print(f"{q}:{response.choices[0].message.content}")

提取信息-返回JSON

JSON是一个形式性很强的格式语言,在后续开发中也会参与网络通信中的数据交换

可以通过单纯的提示词或Few-Shot给模型输出格式,这样就可以直接把得到的JSON拿去处理

这个示例给了若干股市相关文本,让模型提取信息,并按照格式输出

schema = ['日期', '股票名称', '开盘价', '收盘价', '成交量']

examples_data = [
    {
        "content": "2023-08-21,股市震荡。股票蓝天科技今日开盘价为56元,一度上涨至60元,随后回落至55元,最终以58元收盘,成交量达到2100000。",
        "answers": {
            "日期": "2023-08-21",
            "股票名称": "蓝天科技",
            "开盘价": "56元",
            "收盘价": "58元",
            "成交量": "2100000"
        }
    },
    {
        "content": "2024-02-11,市场回暖。股票华夏能源今日开盘价为88元,一度上涨至92元,随后震荡整理,最终以90元收盘,成交量为1850000。",
        "answers": {
            "日期": "2024-02-11",
            "股票名称": "华夏能源",
            "开盘价": "88元",
            "收盘价": "90元",
            "成交量": "1850000"
        }
    },
    {
        "content": "2024-11-05,股市表现强劲。股票未来芯片今日开盘价120元,一度上涨至130元,随后小幅回落,最终以128元收盘,成交量达到4200000。",
        "answers": {
            "日期": "2024-11-05",
            "股票名称": "未来芯片",
            "开盘价": "120元",
            "收盘价": "128元",
            "成交量": "4200000"
        }
    }
]

questions = [
    "2025-03-18,市场波动。股票东方通信今日开盘价为45元,一度上涨至50元,随后回落至47元,最终以49元收盘,成交量达到980000。",
    "2025-07-09,股市上涨。股票星辰医疗今日开盘价为72元,一度上涨至78元,随后小幅回落至75元,最终以76元收盘,成交量为1560000。",
    "2025-12-01,行情平稳。股票新源材料今日开盘价为33元,一度上涨至36元,随后回落至34元,最终以35元收盘。"
]

messages = [
    {"role": "system", "content": "你帮我完成信息抽取,我给你句子,你抽取['日期', '股票名称', '开盘价', '收盘价', '成交量']信息,按JSON格式输出,如果信息不存在,用'原文未提及'表示,请参考下列示例"}
]

for example in examples_data:
    messages.append(
        {"role": "user", "content": example["content"]} # type: ignore
    )
    messages.append(
        {"role": "assistant", "content": json.dumps(example["answers"], ensure_ascii = False)}
    )

for q in questions:
    current_message = messages \
                    + [{"role": "user", "content": f"按照示例,将以下句子转化为所需的JSON格式:{q}"}]

    response = client.chat.completions.create(
        model = "qwen3-max",
        messages = current_message
    )

    print(response.choices[0].message.content)

关联性判断

examples_data = {
    "是": [
        ("公司发布年度财报,净利润同比增长20%。", "财报显示,公司盈利能力明显提升。"),
        ("央行宣布降息政策,刺激经济发展。", "货币政策宽松,有助于经济增长。"),
        ("新能源汽车销量大幅增长,市场需求旺盛。", "新能源行业需求强劲,销量持续上升。")
    ],
    "不是": [
        ("黄金价格上涨,避险情绪升温。", "房地产市场出现下滑趋势。"),
        ("公司宣布裁员计划以降低成本。", "科技创新推动行业发展。"),
        ("油价持续下跌,能源行业承压。", "股市表现强劲,投资热情高涨。")
    ]
}

questions = [
    ("公司利润大幅增长,经营状况良好。", "企业盈利能力增强,财务表现改善。"),
    ("央行加息以抑制通胀。", "货币政策收紧,可能影响经济增长。"),
    ("房地产市场低迷,成交量下降。", "房价上涨带动市场活跃。")
]

messages = [
    {"role": "system", "content": "你帮我完成文本匹配,我给你两个句子,用[]包围,你帮我判断它们是否匹配,回答是或不是,参考以下示例:"}
]

for key, value in examples_data.items():
    for v in value:
        messages.append(
            {
                "role": "user",
                "content": f"判断以下两个句子:[{v[0]}],[{v[1]}]"
            }
        )

        messages.append(
            {
                "role": "assistant",
                "content": key
            }
        )

for q in questions:
    current_message = messages + [{"role": "user", "content": f"判断以下两个句子:[{q[0]}],[{q[1]}]"}]

    response = client.chat.completions.create(
        model = "qwen3-max",
        messages = current_message
    )

    print(f"{response.choices[0].message.content}:[{q[0]}],[{q[1]}]")

综上,Few-Shot就是在限定了要求的基础上进一步压缩了输出空间,并且明确了边界情况,在比较轻量化的功能或者小模型使用时可以使用

Langchain

Langchain是个开源的框架,将大模型交互的接口大幅度简化,同时提供了构建AI调用链的方法

基本使用

Langchain可以通过invoke和stream方法调用模型

from langchain_community.llms.tongyi import Tongyi

model = Tongyi(model = "qwen-max")

res = model.invoke("你能做什么")

print(res)

流式输出的改法与openai差不多

from langchain_community.llms.tongyi import Tongyi

model = Tongyi(model = "qwen-max")

res = model.stream("你能做什么")

for chunk in res:
    print(chunk, end = "", flush = True)

信息注入

其中一种方法是通过langchain_core.messages封装好的接口进行注入:

messages = [
    SystemMessage(content = "你是一个AI助手,你需要简洁地回答用户的问题"),
    HumanMessage(content = "为什么电子书不出盲文版?"),
    AIMessage(content = "因为电子版摸不到"),
    HumanMessage(content = "为什么我吃半碗饭,却要洗一个碗?")
]

res = model.stream(input = messages)

另外langchain本身也支持以元组列表的形式注入,更加方便一些,而且后续也更有用

messages = [
    ("system", "你是一个AI助手,你需要简洁地回答用户的问题"),
    ("human", "为什么电子书不出盲文版?"),
    ("ai", "因为电子版摸不到"),
    ("human", "为什么我吃半碗饭,却要洗一个碗?")
]

提示词模板

一般来说提示词应当是动态的,所以langchain提供了一些提示词模板,可以动态地注入一些信息

提示词模板事实上就是在给出信息之后再去生成prompt

提示词模板都在langchain_core.prompts中,首先最基本的是PromptTemplate

from langchain_core.prompts import PromptTemplate
from langchain_community.llms.tongyi import Tongyi

prompt_template = PromptTemplate.from_template(
    "我的邻居姓{lastname},刚生了个{gender},希望你取个名字,简单回答"
)

prompt_text = prompt_template.format(lastname = "张", gender = "女儿")

model = Tongyi(model = "qwen-max")
res = model.invoke(input = prompt_text)

print(res)

另外,提示词模板也有Few-Shot的模板

from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate
from langchain_community.llms.tongyi import Tongyi

example_template = PromptTemplate.from_template(
    "单词:{word},反义词:{antonym}"
)

example_data = [
    {"word": "大", "antonym": "小"},
    {"word": "上", "antonym": "下"}
]

few_shot_template = FewShotPromptTemplate(
    example_prompt = example_template,
    examples = example_data,
    prefix = "告知我单词的反义词,我提供以下的示例:",
    suffix = "基于前面的示例告诉我,{input}的反义词是?",
    input_variables = ['input']
)

prompt_text = few_shot_template.invoke(input = {'input': '左'})

model = Tongyi(model = "qwen-max")

print(model.invoke(input = prompt_text))

此外,对于历史信息,可以通过MessagePlaceholder占位,然后通过同样的方式注入

import os
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_community.chat_models.tongyi import ChatTongyi

model = ChatTongyi(model = "qwen3-max", api_key = os.getenv("DASHSCOPE_API_KEY"))

chat_prompt_template = ChatPromptTemplate.from_messages(
    [
        ("system", "你是一个AI助手,你需要简洁地回答用户的问题"),
        MessagesPlaceholder("history"),
        ("human", "为什么我吃半碗饭,却要洗一个碗?")
    ]
)

history_data = [
    ("human", "为什么电子书不出盲文版?"),
    ("ai", "因为电子版摸不到"),
    ("human", "我用水兑水,得到的是稀水还是浓水?"),
    ("ai", "同一种物质没有浓或稀的概念")
]

prompt_text = chat_prompt_template.invoke({"history": history_data})

res = model.stream(input = prompt_text)

for chunk in res:
    print(chunk.content, end = "", flush = True)

构建工作流

通过Langchain可以构成模型的工作流

比如上面的Q&A模式可以改造成一个链

chain = RunnableSerializable = chat_prompt_template | model

调用也是同样的形如chain.invoke({'history': history_data})

在这个例子中暂时看不出chain的作用,下面将功能扩展

用取名字的例子,假如我要让模型取名字之后,再让它解读名字的含义,这时候就需要多次使用模型了

理想的构建应该就是

chain = prompt | model | model

可是这时候会报错,因为model调用之后返回的类型不是model需要的输入,要用到langchain_core.output_parsers中的一些转换函数

model = ChatTongyi(model = "qwen3-max", api_key = os.getenv("DASHSCOPE_API_KEY"))
prompt = PromptTemplate.from_template(
    "我邻居姓{lastname},刚生了个{gender},请起名,仅告知名字,不给其他内容"
)
parser = StrOutputParser()

chain = prompt | model | parser | model

其实这个链子对于第二次执行的模型来说有点不知所云,因为只给了一个名字,所以需要进一步拓展

先写两个prompt

first_prompt = PromptTemplate.from_template(
    "我邻居姓{lastname},刚生了个{gender},请起名字,并封装成JSON格式返回给我,"
    "要求key是name,value是起的名字。请严格遵循格式要求"
)

second_prompt = PromptTemplate.from_template(
    "姓名{name},请帮我解析含义"
)

prompt的输入应当是一个含有注入动态信息的JSON,所以需要一个JsonOutputParser

str_parser = StrOutputParser()
json_parser = JsonOutputParser()

chain = first_prompt | model | json_parser | second_prompt | model | str_parser

事实上这个链子还具有可拓展性,可以加上自己定义的一些函数

其中可以用到RunnableLambda接口,以下这个例子就自己实现函数代替了JsonOutputParser

str_parser = StrOutputParser()
myfunc = RunnableLambda(lambda ai_msg: {"name": ai_msg.content})
model = ChatTongyi(model = "qwen3-max", api_key = os.getenv("DASHSCOPE_API_KEY"))

first_prompt = PromptTemplate.from_template(
    "我邻居姓{lastname},刚生了个{gender},只告诉我名字,不要其他额外信息"
)

second_prompt = PromptTemplate.from_template(
    "姓名{name},请帮我解析含义"
)

chain = first_prompt | model | myfunc | second_prompt | model | str_parser

还有一个问题需要解决,就是有时候可能需要产生多支路的有向无环图,但是单条链肯定是做不到

设想一个场景,同样的信息要同时给到两个组件,并且这两个组件也要一起把信息给下一个组件,如果用单条链的方式,肯定有其中一个组件接收不到输入,所以RunnablePassThrough()实例占位可以解决这个问题

chain = (
	{
		'input': RunnablePassthrough(),
		'context': RunnableLambda(format_for_retriever) | retriever | format_document
    } | RunnableLambda(format_for_prompt_template) | self.prompt_template | print_prompt | self.chat_model | StrOutputParser()
)

这相当于用字典封装了一块子单元,然后子单元可以再去设计子工作流

这样对于DAG上的线性任务就基本能完成了

对话记忆

在真实的应用场景中,需要在对话过程中把对话内容放进对话历史中

Langchain提供了RunnableWithMessageHistory完成这个要求

基本实现

暂时用一个字典去存不同会话的对话历史

chat_history_store = {}

def get_history(session_id):
    if session_id not in chat_history_store:
        chat_history_store[session_id] = InMemoryChatMessageHistory()
    return chat_history_store[session_id]

接着在链子上就可以用一个RunnableWithMessageHistory接口了

conversation_chain = RunnableWithMessageHistory(
    base_chain,
    get_history,
    input_messages_key = "input",
    history_messages_key = "chat_history"
)

在invoke的时候,由于get_history本身有一个session_id要传,所以需要给出

if __name__ == "__main__":
    session_config = {"configurable": {"session_id": "user_001"}}

    print(conversation_chain.invoke({"input": "小明有一只猫"}, session_config))
    print(conversation_chain.invoke({"input": "小刚有两只狗"}, session_config))
    print(conversation_chain.invoke({"input": "共有几只宠物?"}, session_config))

记忆持久化

前一部分的功能在程序结束之后记忆会消失

所以需要写一个类,将记忆存储在服务器的文件中,这样就可以在程序重启后也能有对应session的记忆了

class FileChatMessageHistory(BaseChatMessageHistory):
    def __init__(self, session_id, storage_path):
         self.session_id = session_id
         self.storage_path = storage_path

         self.file_path = os.path.join(self.storage_path, self.session_id)

         os.makedirs(os.path.dirname(self.file_path), exist_ok=True)

接着有三个函数是基类BaseChatMessageHistory要求重写的

首先有两个json库的函数要说一下:

  • json.load/json.loads:将json转化为python的对象,前者为文件操作,后者为字符串操作
  • json.dump/json.dumps:将python对象转化为json格式,前者存进文件,后者得到字符串

第一个是add_message,负责写入新信息,在实现中是把新信息拼接在读取的旧信息之后

def add_messages(self, messages: Sequence[BaseMessage]) -> None:
    all_messages = list(self.messages)
    all_messages.extend(messages)

    new_messages = [message_to_dict(message) for message in all_messages]

    with open(self.file_path, "w", encoding = "utf-8") as f:
        json.dump(new_messages, f)

接着是messages函数,负责读取所有历史,由于这个函数的语义其实是个元素,所以给了个属性的注解,可以用obj.messages直接调用而不是加括号

@property
def messages(self) -> list[BaseMessage]:
    try:
        with open(self.file_path, "r", encoding = "utf-8") as f:
            messages_data = json.load(f)
            return messages_from_dict(messages_data)
    except FileNotFoundError:
        return []

最后是clear函数,负责处理会话历史,在实现中就是把列表置空

def clear(self) -> None:
    with open(self.file_path, "w", encoding = "utf-8") as f:
        json.dump([], f)

相应地,get_history函数也要改

def get_history(session_id):
    return FileChatMessageHistory(session_id = session_id, storage_path = "./chat_history")

RAG

RAG(Retrieval-Augmented Generation),检索-增强-生成,看起来很神秘,其实依旧是针对提示词的一个开发技巧

作为底座的大模型具有很多知识,但是这些知识在特定领域里可能过旧甚至是不可控,为了减少这种情况,RAG的解决思路就是给大模型一些参考资料,作为prompt注入到对话中

具体的流程如下:

准备流程:
	上传文本 --> 文本分割 --> 向量化 --> 存进数据库
	
在线流程:
	得到用户问题 --> 问题文本向量化 --> 与数据库中的向量比对相似度 --> 取出相似度最高的top-k注入prompt

下面用一个实战项目的RAGService讲解流程

首先是准备流程,将上传的文本分割向量化,数据会存在SQLite里面:

class KnowledgeBaseService(object):
    def __init__(self):
        os.makedirs(config.persist_directory, exist_ok = True)

        self.chroma = Chroma(
            collection_name = config.collection_name,
            embedding_function = DashScopeEmbeddings(model = 'text-embedding-v4'),
            persist_directory = config.persist_directory
        )  # 向量存储实例
        self.spliter = RecursiveCharacterTextSplitter(
            chunk_size = config.chunk_size,
            chunk_overlap = config.chunk_overlap,
            separators = config.separators,
            length_function = len
        ) # 文本分割器对象

    # 向量化 -> 存入数据库
    def upload_by_str(self, data: str, filename):
        md5_hex = get_str_md5(data)

        if check_md5(md5_hex):
            return '[跳过]内容已存在知识库中'

        if len(data) > config.max_split_char_number:
            knowledge_chunks = self.spliter.split_text(data)
        else:
            knowledge_chunks = [data]

        metadata = {
            'source': filename,
            'create_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'operator': 'admin'
        }

        self.chroma.add_texts(
            knowledge_chunks,
            metadatas = [metadata for _ in range(len(knowledge_chunks))]
        )

        save_md5(md5_hex)

        return '[成功]内容已成功载入向量库'

接着就是在线的服务

class RagService(object):
    def __init__(self):
        self.vector_service = VectorStores(
            embedding = DashScopeEmbeddings(model = config.embedding_model_name)
        )

        self.prompt_template = ChatPromptTemplate.from_messages(
            [
                ('system', '以我提供的已知参考材料为主,简洁专业地回答用户的问题。参考资料:{context}。'),
                ('system', '并且我提供用户对话的历史记录,如下:'),
                MessagesPlaceholder('history'),
                ('user', '请回答用户提问:{input}')
            ]
        )

        self.chat_model = ChatTongyi(model = "qwen3-max", api_key = os.getenv("DASHSCOPE_API_KEY"))

        self.chain = self.__get_chain()

__get_chain()函数用来获取工作流

最终构建出如下的基本链:

chain = (
    {
        'input': RunnablePassthrough(),
        'context': RunnableLambda(format_for_retriever) | retriever | format_document
    } | RunnableLambda(format_for_prompt_template) | self.prompt_template | print_prompt | self.chat_model | StrOutputParser()
)

其中'input'和'contex'分别对应用户的提问和知识库

两个lambda函数都是为了对齐输入格式写的

def format_for_retriever(value: dict) -> str:
    return value['input']
def format_for_prompt_template(value):
    new_value = {}
    new_value['input'] = value['input']['input']
    new_value['context'] = value['context']
    new_value['history'] = value['input']['history']
    return new_value

到这里,应用其实已经结束了,可以直接调用测试,比如提前提供了穿搭推荐的知识库之后:

if __name__ == '__main__':
    session_config = {
        'configurable': {'session_id': 'user001'}
    }
    res = RagService().chain.invoke({'input': '春天穿什么颜色'}, session_config)
    print(res)

Agent

在前面已经说过,Langchain可以实现DAG的工作流

但是现实中的工作不一定可以从头走到尾就完成,可能需要经过一些工作的循环

这就是Agent做的事情,它能够自主地做出选择,然后工作可以不局限于一个DAG

langchain.agents

langchain.agents提供了创建agent的接口,需要把能使用的工具暴露给大模型

工具用@tool注解给出

@tool (description = '查询天气')
def get_weather():
    return '晴天'

接着就可以按照与先前相似的操作调用方法

agent = create_agent(
    model = ChatTongyi(model = "qwen3-max", api_key = os.getenv("DASHSCOPE_API_KEY")),
    tools = [get_weather],
    system_prompt = '你是一个聊天助手,可以回答用户问题'
)

res = agent.invoke(
    {
        'messages': [
            {'role': 'user', 'content': '明天天气如何?'}
        ]
    }
)

for msg in res['messages']:
    print(msg.content)

ReAct框架

ReAct框架是一种工作范式,流程如下:

思考 --> 行动 --> 观察 --> 再思考

其实这就是agent的工作模式,但是可以在提示词中强调,看看agent的工作流程

比如让agent计算bmi,并且给一点干扰函数

@tool(description = '获取股价,传入股票名称,返回字符串信息')
def get_price(name: str) -> str:
    return f'股票{name}的股价是20元'

@tool(description = '获取股票信息,传入股票名称,返回字符串信息')
def get_info(name: str) -> str:
    return f'股票{name}是一家上市公司,专注于IT职业教育'

@tool(description = '获取体重,返回整数,单位千克')
def get_weight() -> int:
    return 80

@tool(description = '获取身高,返回身高,单位厘米')
def get_height() -> int:
    return 180

接着给提示词并且看调用情况

agent = create_agent(
    model = ChatTongyi(model = "qwen3-max", api_key = os.getenv("DASHSCOPE_API_KEY")),
    tools = [get_price, get_info, get_weight, get_height],
    system_prompt = """
        你是严格遵循ReAct框架的智能体,必须按照[思考->行动->观察->再思考]的流程解决问题,
        且**每轮仅能思考并调用1个工具**,禁止单次调用多个工具。
        请告诉我你的思考过程,工具的调用原因,按思考、行动、观察三个结构告知我
    """
)

for chunk in agent.stream(
    {
        'messages': [
            {'role': 'user', 'content': '计算我的BMI'}
        ]
    },
    stream_mode = 'values'
):
    latest_message = chunk['messages'][-1]

    if latest_message.content:
        print(type(latest_message).__name__, latest_message.content)

    try:
        if latest_message.tool_calls:
            print(f"工具调用:{ [tool['name'] for tool in latest_message.tool_calls] }")
    except AttributeError as e:
        pass

其中有如下的片段:

HumanMessage 计算我的BMI
AIMessage [思考]  
要计算BMI(身体质量指数),需要知道体重(单位:千克)和身高(单位:米)。公式为:  
**BMI = 体重 / (身高^2)**  

当前我还不知道用户的体重和身高,因此首先需要获取体重数据。

[行动]  
调用工具 `get_weight` 获取体重。


工具调用:['get_weight']
ToolMessage 80
AIMessage [观察]  
获取到用户的体重为 80 千克。

[思考]  
现在已知体重为 80 千克,接下来需要获取用户的身高(单位:厘米),以便转换为米并计算 BMI。

[行动]  
调用工具 `get_height` 获取身高。

中间件

在agent的工作过程中,为了监听工作情况,可以用一些中间件去查看状态,或是做一些操作,示例如下:

@before_agent
def log_before_agent(state: AgentState, runtime: Runtime) -> None:
    print(f"[before agent]agent启动,并附带{len(state['messages'])}条信息")

@after_agent
def log_after_agent(state: AgentState, runtime: Runtime) -> None:
    print(f"[before agent]agent结束,并附带{len(state['messages'])}条信息")

@before_model
def log_before_model(state: AgentState, runtime: Runtime) -> None:
    print(f"[before model]model即将调用,并附带{len(state['messages'])}条信息")

@after_model
def log_after_model(state: AgentState, runtime: Runtime) -> None:
    print(f"[after model]model结束调用,并附带{len(state['messages'])}条信息")

@wrap_model_call
def model_call_hook(request, handler):
    print('模型已调用')
    return handler(request)

@wrap_tool_call
def tool_call_hook(request, handler):
    print(f"工具执行:{request.tool_call['name']}")
    print(f"工具传入参数:{request.tool_call['args']}")

    return handler(request)

下面试着用了现实中的api测试:

@tool(description = """
    用高德行政区域查询接口,根据地点名称获取 adcode。

    参数:
        keywords: 地点关键词,例如 "北京"、"北京市朝阳区"、"天河区"
        filter_adcode: 可选,上级行政区 adcode,用于减少歧义,例如北京是 "110000"

    返回:
        匹配到的第一个行政区 adcode(字符串)

    异常:
        ValueError: 高德返回失败、没有匹配结果、或结果里没有 adcode
        requests.RequestException: 网络请求失败
    """)
def get_adcode(keywords: str, filter_adcode: str | None = None) -> str:
    url = "https://restapi.amap.com/v3/config/district"
    params = {
        "key": 'my_key',
        "keywords": keywords,
        "subdistrict": 0,     # 不返回下级行政区,拿当前结果就够了
        "extensions": "base", # 不需要边界数据
        "output": "JSON",
    }

    if filter_adcode:
        params["filter"] = filter_adcode

    resp = requests.get(url, params=params, timeout=10)
    resp.raise_for_status()
    data = resp.json()

    if data.get("status") != "1":
        raise ValueError(f"Amap API error: {data.get('info', 'unknown error')}")

    districts = data.get("districts") or []
    if not districts:
        raise ValueError(f"未找到地点: {keywords}")

    first = districts[0]
    adcode = first.get("adcode")
    if not adcode:
        raise ValueError(f"查询成功,但结果里没有 adcode: {first}")

    return adcode

@tool(description = '查询天气,传入行政区域码,返回JSON格式的天气信息')
def get_weather(city_code: str):
    url = "https://restapi.amap.com/v3/weather/weatherInfo"
    params = {
        "key": 'my_key',
        "city": city_code
    }

    response = requests.get(url, params=params, timeout=10)
    response.raise_for_status()
    return response.json()

以上是用了高德地图的api,主要是要在description里面详细讲函数的传参规则,这也算是提示词工程的一部分

agent = create_agent(
    model = config.model,
    tools = [get_weather, get_adcode],
    system_prompt = """
        你是严格遵循ReAct框架的智能体,必须按照[思考->行动->观察->再思考]的流程解决问题,
        且**每轮仅能思考并调用1个工具**,禁止单次调用多个工具。
        请告诉我你的思考过程,工具的调用原因,按思考、行动、观察三个结构告知我
    """,
    middleware = [log_before_agent, log_after_agent, log_before_model, log_after_model, model_call_hook, tool_call_hook],
)

for chunk in agent.stream(
    {
        'messages': [
            {'role': 'user', 'content': '我在长春,今天朝阳区天气如何,应该怎么穿衣?'}
        ]
    },
    stream_mode = 'values'
):
    latest_message = chunk['messages'][-1]

    if latest_message.content:
        print(type(latest_message).__name__, latest_message.content)

示例中用了有同名区域的朝阳区测试,为了看agent是否能够都明白描述,结果令人满意:

HumanMessage 我在长春,今天朝阳区天气如何,应该怎么穿衣?
[before agent]agent启动,并附带1条信息
[before model]model即将调用,并附带1条信息
模型已调用
AIMessage [思考]  
用户想知道长春市朝阳区今天的天气情况,并据此获得穿衣建议。为了获取天气信息,首先需要知道朝阳区的行政区域码(adcode)。由于“朝阳区”在全国可能有重名(如北京市也有朝阳区),应使用上级行政区“长春市”作为过滤条件,以确保获取的是长春市朝阳区的 adcode。

[行动]  
调用 `get_adcode` 工具,传入关键词 "朝阳区",并用 "长春" 的 adcode 作为 filter_adcode。但此时我还不知道长春市的 adcode,因此第一步应先获取“长春”的 adcode。


[after model]model结束调用,并附带2条信息
工具执行:get_adcode
工具传入参数:{'keywords': '长春'}
ToolMessage 220100
[before model]model即将调用,并附带3条信息
模型已调用
AIMessage [观察]  
成功获取到长春市的 adcode 为 "220100"。

[思考]  
现在可以利用这个 adcode 作为上级区域过滤,准确查询长春市下辖的“朝阳区”的 adcode。

[行动]  
调用 `get_adcode` 工具,传入关键词 "朝阳区",并设置 filter_adcode 为 "220100"。


[after model]model结束调用,并附带4条信息
工具执行:get_adcode
工具传入参数:{'keywords': '朝阳区', 'filter_adcode': '220100'}
ToolMessage 220104
[before model]model即将调用,并附带5条信息
模型已调用
AIMessage [观察]  
成功获取到长春市朝阳区的 adcode 为 "220104"。

[思考]  
现在有了朝阳区的行政区域码,可以调用天气查询接口获取该地区的天气信息,从而为用户提供穿衣建议。

[行动]  
调用 `get_weather` 工具,传入 city_code 为 "220104"。


[after model]model结束调用,并附带6条信息
工具执行:get_weather
工具传入参数:{'city_code': '220104'}
ToolMessage {"status": "1", "count": "1", "info": "OK", "infocode": "10000", "lives": [{"province": "吉林", "city": "朝阳区", "adcode": "220104", "weather": "多云", "temperature": "11", "winddirection": "西", "windpower": "≤3", "humidity": "21", "reporttime": "2026-04-21 21:03:45", "temperature_float": "11.0", "humidity_float": "21.0"}]}
[before model]model即将调用,并附带7条信息
模型已调用
AIMessage [观察]  
获取到长春市朝阳区当前天气信息如下:  
- 天气:多云  
- 气温:11°C  
- 风向:西风  
- 风力:≤3级  
- 湿度:21%(较干燥)  
- 报告时间:2026-04-21 21:03:45  

[思考]  
当前气温为11°C,属于春季偏凉的天气,且空气干燥、风力不大。建议用户穿着长袖衣物,如薄外套或针织衫,搭配长裤;由于湿度低,可适当补水并注意皮肤保湿。

[最终回答]  
今天长春朝阳区天气多云,气温11°C,西风≤3级,空气较干燥。建议穿长袖衬衫、薄外套或卫衣,搭配长裤;同时注意补水和保湿。
[after model]model结束调用,并附带8条信息
[before agent]agent结束,并附带8条信息
posted @ 2026-04-21 21:27  R4y  阅读(12)  评论(0)    收藏  举报