本章涵盖的内容(This chapter covers)
- 为结构化研究构建 AI 代理
- 使用 LangChain 构建 AI 代理
- 复用强提示词以获得可比研究
- 将研究结果导出到 Notion
第 8 章总结:大语言模型(LLM)是强大的研究助手,能够显著简化资产分析。我们学习了基础知识,包括判别式 AI与**生成式 AI(GenAI)**的区别,以及如何有效触发 LLM 提示词。
第 9 章首先聚焦在不依赖框架的 LLM 高级用法,用以构建 AI 代理。我们将展示把 LLM 集成到工作流中的场景;创建一个提示词仓库并设计首个工作流,为“AI 增强研究”带来更多结构化。同时,我们还会演示如何把研究发现导出到 Notion(一款帮助高效组织与结构化研究的现代笔记应用)。
本章第二部分将集成框架来构建 AI 代理:我们会讲如何使用它们、并展示它们能把多少工作简化。我们会介绍无代码平台 n8n 能实现什么;最后聚焦 LangChain,它允许我们用 Python 编写逻辑。开始吧。
9.1 需求(Requirements)
在第 8 章中,我们采用了一次性提示(one-shot prompting)去查询 LLM。简单说,就是提交单个提示换取单次回应。现在想象你得到一次与人类专家沟通的机会,却只能问一个问题——这很常见,比如记者会中政客对媒体的答问。若问题表达不佳,你可能得不到想要的答案。与“单问单答”相比,下一阶段是对话式沟通:基于先前回答,你可以继续提出更有针对性的问题。
9.1.1 成功的沟通(Successful communication)
把这类情境类比到 AI:除了在基础场景下的一次性发问,我们还可以为更复杂的设置制定更精细的策略。深入细节前,先看看在这个类比中实现成功沟通需要什么。
记忆(MEMORY)
人与 AI 对话的一个差异是对既往交流的反思能力。若 Rajesh 与 Peter 讨论了复杂话题并获得新洞见,之后的交流中两人都能利用这些收获。而在当前发展阶段,LLM 不会因为与用户互动而自动获得新增洞见。
NOTE 在后台,LLM 开发者无疑会分析模型与用户的对话,以将所得洞见用于新模型的训练。但这与人类从一次讨论中“学会”某事并不相同。
一个务实做法是使用 RAG(检索增强生成) :通过把相关外部上下文并入模型输入来增强 LLM。换言之,我们通过添加特定信息为 LLM 创建额外记忆,以便其在生成答案时调用。
语言能力(LANGUAGE SKILLS)
想象你被“锁”在梵蒂冈图书馆里,里面是秘藏了数百年的书籍,蕴含迷人的历史洞见——但它们都用你不懂的古语言写成。你需要翻译工具或专家的帮助。
LLM 训练于海量数据;训练范围之外的数据对它而言就像我们从未学过的拉丁语一样“不可达”。无论 LLM 还是人类,学习新能力都需要时间(训练新模型、学习拉丁文)。既然人类可以借助外部帮助,LLM 也一样:一种方式是在提示时把这些“训练期不可用的信息”并入输入。
我们已经提到可以为 LLM 提供数据库访问,以整合补充数据并确保模型能接入其他来源。除了显而易见的网页搜索外,还可纳入各类数据提供商 API,让 AI 代理去获取邮箱内容或即时通讯聊天记录。我们可以让 AI 代理解析这些来源——通常通过携带访问令牌的 API 调用——再把信息提供给 LLM。
策略(STRATEGY)
想象把许多聪明才智聚在一室解决一个复杂问题。有时这些“高能大脑”会在缺乏策略或协调的情况下畅想点子,却难以产出可落地的方案。反而是有目标的、适度协调的团队更可能胜过一群“无舵之才”。
当多名“创意个体”参与时,常常需要一个领导者去有效引导他们的集体创造力,并激励他们协同。Jim Simons 创建了最成功的投资基金之一。人们敬佩他不仅因为招募了常春藤名校的天才,更在于协调这些天才往往比“构造获利数学模型”更难。一旦这些“锐脑”被对齐,Renaissance Technologies 管理的基金便能跑赢市场。
熟悉学术体系的人也懂:缺乏同行评审的论文会被期刊降权。即使最聪明的科学家也需要反馈;难点往往不是“想出主意”,而是把细枝末节打磨清楚。
对 AI 亦然:我们可以使用多个专门 LLM并让它们各司其职:一个负责分析,另一个负责同行审阅。我们可以定义整套逻辑,让 AI 如同一支管弦乐队,每个 LLM 演奏好自己的部分,合奏出一致的结果。
9.1.2 代理式设计模式(Agentic design patterns)
使用代理式设计模式是为 AI 研究搭结构的一种方法。DeepLearning.AI(mng.bz/V9Qx)提出了四种模式,我们将详细讨论:
- 反思(Reflection)
- 工具使用(Tool use)
- 规划(Planning)
- 多代理协作(Multi-agent collaboration)
反思(REFLECTION)
反思模式让代理通过分析过往行为并基于反馈调整策略来迭代提升表现。它鼓励自我评估:任务结束后,代理收集成果数据并对照预设目标/指标评估表现。经由反复的反馈回路,代理识别低效点、发现模式,从而优化决策。
以交易机器人为例:交易日内按预设策略下单;收盘后汇总收益性、风险暴露、执行准确度等指标,评估哪些策略表现不佳或错过信号,并据此修订次日策略——例如收紧风险阈值、降低低收益策略权重、微调执行时机。行动–评估–校正的持续循环正是反思模式的核心。
工具使用(TOOL USE)
工具使用模式通过集成外部工具/库/API扩展代理功能。代理不只依赖内部逻辑,可把特定任务委托给专门系统,如数据检索、转换或复杂计算。
这就像人类用计算器/网络服务高效处理领域问题。AI 代理可调用股价 API抓取实时数据,或唤起情感分析工具解读市场新闻。必要时,代理能自主识别应调用的工具、编排调用并把结果纳入更广泛的决策流程。
把复杂子任务外包给“即用型工具”,可让代理保持轻量与模块化,从而多面应战、避免重复造轮子。接下来看看如何协调这些子任务。
规划(PLANNING)
规划模式让代理为复杂目标构建并执行多步骤策略,根据当前目标、资源与约束动态适配工作流。
做法是:代理先定义终极目标,评估环境、识别约束/依赖/工具;若首选资源不可用,则寻求替代。基于评估,构造考虑依赖关系的行动序列并逐步执行,同时持续监控结果。
例如,一个 AI 驱动的选股系统每天评估数百家公司。为识别有潜力的投资,它动态搭建研究工作流:收集财务数据、扫描新闻、处理情感数据并排序机会。若某公司因业绩意外而“画像”突变,代理会重排优先级并发送及时预警。
规划模式强调有结构的灵活性,让“被动反应”的代理转变为“主动谋划”的策略家。接下来看看多代理如何协同工作。
多代理模式(MULTI-AGENT PATTERN)
多代理模式协调多个专长各异的代理协作解决复杂任务,强调并行、专精与分布式推理。一个主持者/协调者统筹流程,确保代理协同并有效整合输出。
把“买入决策”想象成法庭式审议:一个代理担任乐观派,从增长、盈利与动量论证买入;第二个代理扮演怀疑派,聚焦下行风险、宏观逆风或估值过高;第三个中立代理为裁决者,审阅双方论据、评估权重,综合形成最终建议。这类似人类决策中的专家小组,通过多元视角与模块化分工提高稳健性。
一个参考工作流(A REFERENCE WORKFLOW)
为了把上述要点整合到一个 AI 代理的参考案例中,我们构建一个投资分析工作流(见图 9.1)。
图 9.1 AI 代理的参考工作流
第一阶段需要触发工作流。可能方式包括:
- 定时事件——每周一早上,定时任务触发一个提示:查询上周可能影响用户投资组合持仓的事件;
- 用户提问——用户想重新评估其组合风险,提交提示以识别哪些资产更易受某次政治危机影响。我们可从常见即时通讯应用喂给 AI 代理:例如 WhatsApp、Signal、Telegram 等;
- 警报——当市场出现剧烈波动时,预先定义的警报触发工作流,自动提出关于市场活动及其对用户组合影响的具体问题。
一个编排器会从检索开始。在图 9.1 中我们展示了一条工作流;当然,代理不局限于单一路径。第一步是信息检索。工程师为各 LLM 版本训练的数据截至某一时点;模型发布后,团队会忙于新模型,通常不会再改动旧模型。RAG 为 LLM 提供新增数据,弥合模型发布日期与提示提交时间之间的差距。对此用例,我们定义三类来源:
- RAG 数据库——额外的作业从不同来源收集数据并写入向量数据库,使工作流能把这些数据提供给 LLM 生成流程。需注意这意味着新增组件的运维;数据摄取任务会把多源数据灌入 RAG 库。
- 网页搜索——AI 代理通常提供标准化的搜索接口,以利用搜索引擎返回的内容来生成回答。
- 模型上下文协议(MCPs, Model Context Protocols) ——这是一种新接口,让 AI 代理以标准化格式连接并采集数据源。可把 MCP 视作专为 LLM 设计的 API。例如需要最新股价时,LLM 可在生成回答的同时查询该信息。相较 RAG,一个重要优势是:MCP 越多,我们就越无需自建数据聚合作业。
数据到手后,就进入生成环节。我们可以考虑多模型协作生成答案,方式包括:
- 同题多模:给不同 LLM 同一提示。由于训练方式与数据不同,看看它们是否带来不同洞见;
- 分工多模:给不同 LLM 不同提示。在投资分析中,一个负责多头情境(bullish),另一个负责空头情境(bearish);
- 当然也可混合。提示越多,结果越多;减少提示的主要原因是成本——每次生成都要花钱。
“标准的仅 RAG思路”是:用一个 LLM 在增强后的来源上收集并生成结果。但若有多个 LLM,我们还可引入额外评审阶段:用另一模型汇总审阅多模型输出。最后是输出:如第 8 章所示,我们可以把所有结果存入数据库(如 Notion),或任何你常用的渠道。
我们可以把所有阶段映射到代理式设计模式上,并对生成结果进行反思;使用各类工具为 LLM 收集额外数据、规划执行、按需调整工作流,并调度不同 LLM协同完成。
9.2 不依赖框架的代理式工作流(Agentic workflows without frameworks)
下面展示如何不使用专门框架也能推进代理式工作流。我们将把数据科学笔记本用于临时性研究,并目标是把研究自动化、结构化、可复用。重点聚焦三个方面:
- 建立可复用提示词仓库;
- 连接不同的 LLM;
- 导出结果。
我们的目标之一是尽可能保持简单。本书每一章都提供可在出版社网站下载的完整可运行代码。你还会在第 9 章引用的笔记本中,找到第 8 章讲到的“调用 LLM 的代码片段”。开始吧。
9.2.1 提示词仓库(Prompt repository)
通过持续分析公司,我们可以沉淀出可复用的提示词,适配多种场景。例如:我们可以让 LLM 分析 Apple 的业绩参数;随后把同一个提示用于 Alphabet 或 Amazon。在第 8 章我们学习了如何优化提示词;在此基础上,我们可以更新与打磨现有提示,并将其映射到不同场景。
提示词也可以存储到数据库以便复用。先看一个尽可能简单的数据库结构:
- ID ——用于标识提示词。示例:
porter_5_forces_framework或swot_analysis。ID 直观表明提示词主题; - Prompt ——提示词正文,需足够精准以唤起 LLM 的期望回答。花时间打磨问题;若 LLM 没给到理想答案,就继续改进。你也可以在正文中放占位符,后续再替换。
正如《银河系漫游指南》的粉丝都懂的:问对问题并不容易。关键在于搭建扎实的提示库并按需迭代。例如,下面是一个最基础的提示:
作为投资者,我正在关注 {company}。请给出一份 BCG(波士顿矩阵) 分析,并说明该公司位于哪个象限。
其中花括号包裹的 company 是一个需要先定义的变量,我们可以用 Python f-string 将变量值注入到字符串中。按需要继续细化与扩展此提示以增加上下文;出于教学目的,这里保持简洁。
每条提示可以对应多个上下文。例如,上述提示既可用于“收益型”也可用于“成长型”选股的初步探索;但如果我们对某家公司已经非常熟悉,再问“它在 BCG 哪个象限”就不再增值。
当数据库中提示很多时,可以通过标签(tag)为其添加检索上下文,便于后续快速定位。你可能会按行业/板块复用提示。第 11 章我们会讨论“催化剂”(可能导致股价显著变化的事件);一些提示可以针对特定催化剂(如财报电话会)复用。提示越具体(记住:问得越具体,答得越具体),适用场景就越窄。
如果单独维护一个“标签表”,即可在表间建立 n:m 关系:一个标签可关联多条提示,一条提示也可对应多个标签。
现在若要把提示与标签写入数据库表,可以使用如下 建表语句(清单 9.1)。这些表被精简到最简形态,便于从本地数据库随取随用。注意:执行 DDL 语句需要一套已连接数据库的 SQL 控制台;用于探索,SQLite 已绰绰有余。
清单 9.1 创建提示词仓库的 DDL 脚本
create table prompt
(
id TEXT primary key,
prompt TEXT not null
);
create table main.tag
(
tag_name TEXT not null,
prompt_id TEXT not null
);
这些表可以用于交互式研究。我们在开发环境里打开 Jupyter Notebook,可能先研究某一只股票(比如 Apple),随后切换到其他股票(例如 Alphabet)。若提示保存在数据库中,随时都能重新加载提示文本并发送给 LLM。
有了这些,我们就能取出提示→发送给 LLM(上一章已演示发送方式)→得到大量回答。接下来需要把这些信息安全存放。下面看看怎么做。
9.2.2 导出结果(Export results)
用过多个 LLM 聊天机器人的朋友会很熟悉这种情况:你在使用多家 LLM,过一段时间后,提示与回答散落在各个平台;不同话题的提问交织在一起,想回看过去的某条结果就会很费劲。
一个预防办法是把输出存入统一的中心库。第 6 章中我们用 Google Sheets 监控过投资组合;而在本章的研究里,我们使用结构化笔记应用。
Notion 是一款专业的笔记应用,它允许你为单一主题(如投资研究)创建多页的研究日志(journal),你可以通过标签高效筛选,并按最后编辑时间或创建日期等属性排序。
图 9.2 展示了一个 Notion 研究日志,其中包含两个组合。在页面顶部的 URL 中有一个唯一 ID,如:1a9bc3d9e2cd809ca869f99516383ccc。当你在 Python 中创建带研究日志的“笔记本”并通过 Web 分享时,每个页面都会有一个唯一标识符,可在 URL 中看到。
图 9.2 一个包含若干参考条目的 Notion 研究日志。你可以把 Notion 作为整合多家 LLM 输出的统一存储。
下面学习如何把内容导出到 Notion。首先需要标题。示例截图里有类似 INVZ—Initial Investment Thesis 的参考条目。Notion 需要一个包含标题的字典结构来创建页面标题。以下方法给出一个基础模板:我们将字符串形式的 title 作为参数传入,然后把该标题分别写入 text.content 与 plain_text 中:
def get_title_property(title: str):
return {'Name': {'id': 'title',
'type': 'title',
'title': [{'type': 'text',
'text': {'content': title, 'link': None},
'annotations': {'bold': False,
'italic': False,
'strikethrough': False,
'underline': False,
'code': False,
'color': 'default'},
'plain_text': title,
'href': None}]}}
有了 title property,我们就可以创建页面。如下代码通过 REST 调用创建页面,并把标题属性作为参数传入;收到返回码后检查是否为 200 以确认成功。参数 DATABASE_ID 即我们通过 URL 获取到的唯一 ID(见本节开头):
def create_page(title_properties: dict) -> str:
create_url = "https://api.notion.com/v1/pages"
payload = {"parent": {"database_id": DATABASE_ID},
"properties": title_properties}
res = requests.post(create_url, headers=headers, json=payload)
if res.status_code == 200:
print(f"{res.status_code}: Page created successfully")
else:
print(f"{res.status_code}: Error during page creation")
return res.json()["id"]
返回值是一个页面 ID,对应新建页面。有了它,我们就能往该页面写入内容(如下)。依旧通过 REST API,以 JSON 兼容字典提交:
def write_to_page(page_block_id: str, data: dict) -> bool:
edit_url = f"https://api.notion.com/v1/blocks/{page_block_id}/children"
res = requests.patch(edit_url, headers=headers, json=data)
if res.status_code != 200:
print(f"{res.status_code}: Error during page editing")
return False
return True
写入 Notion 时要注意:一次最多写入 2,000 个字符。因此需要先把文本分块再写入。下面的方法演示了这种做法;若某次写入失败,就中断整个流程:
def write_chunked(page_block_id, text, chunk_size = 2000):
for i in range(0, len(text), chunk_size):
chunk = text[i:i+chunk_size]
if not write_to_page(page_block_id, create_block(chunk)):
break
内容同样需要封装为 JSON 兼容字典,以覆盖格式与其他属性。上面的 write_to_page 会调用 create_block 来创建此字典模板,并将文本写入 content 字段:
def create_block(content : str) -> dict:
return {
"children": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": content,
},
}
]
},
},
]
}
把这些拼起来后,就可以封装成一个提问例程。清单 9.2 展示了向 LLM 提交提示并把结果写入 Notion 的代码。笔记标题由股票代码 + 当前日期 + 提示 ID + 使用的 LLM拼接而成。
清单 9.2 提交提示(Submitting a prompt)
from datetime import datetime
def ask_question(stock_ticker: str, prompt_id: str,
prompt_text = None, llm = LLM.OPENAI) -> None:
date_str = datetime.now().strftime("%Y_%m_%d")
if stock_ticker:
properties = get_title_property(
f"{stock_ticker}-"
f"{date_str}-{prompt_id}-{llm}") #1
else:
properties = get_title_property(
f"generic-{date_str}-{question_id}-{llm}") #1
page_block_id = create_page(properties)
if prompt_text is None: #2
prompt_text = get_question_by_id(question_id)
if len(prompt_text) > 0:
write_chunked(page_block_id, prompt_text) #3
write_chunked(page_block_id,
get_answer(stock_ticker,
prompt_text,
llm)) #4
#1 获取标题属性
#2 若不从数据库取,也可直接传入文本
#3 先写入“提示”
#4 再写入 LLM 的“回答”
图 9.3 展示了当我们持续提交提示时,Notebook 的可能呈现。LLM 返回答案后,我们可在 Notion 中打开这些笔记进行事实核查。
图 9.3 把投资研究的响应统一存放在 Notion 的参考内容页
研究往往是交互式的。在数据科学笔记本中,我们一边编码一边向 LLM 发送提示;同时从数据库拉取信息,按标签过滤,并通过 ID 获取提示。清单 9.3 给出了相关方法:它返回与指定主题有关的所有提示及其 ID。在参考案例中,主题 ID 为 earnings_call。
清单 9.3 获取某一主题的所有提示(Getting all prompts for a topic)
engine = create_engine('sqlite:///question.db')
all_questions = pd.read_sql('prompt', engine)
all_prompts = pd.read_sql('context', engine)
all_tags = pd.read_sql('tag', engine)
def get_prompt_by_id(id: str) -> str: #1
try:
return all_prompts[all_prompts["id"] == id]["prompt"].iloc[0]
except:
return "No prompt found with that ID"
def get_prompts_per_tag(tag: str): #2
all_prompt_tags = all_tags.loc[
all_tags['tag_name'] == tag,
'prompt_id'].tolist()
return all_prompts[all_prompts['id'].isin(all_prompt_tags)]
all_prompts_per_category = get_prompts_per_category("earnings_call")
all_prompts_per_category
#1 通过 ID 获取提示
#2 获取该标签下的全部提示
随后,调用 get_prompt_by_id("earnings_call") 即可获取 ID 为 "earnings_call" 的提示文本(例如:“公司即将召开财报电话会。为了决定是否减仓或加仓,我应检查哪些要点?”)。直接查看 all_prompts_per_category 可能不够,因为在屏幕上显示时提示文本可能会被截断。
有了这些,只需用正确参数(如股票代码与提示 ID)调用 ask_question,就能把内容写满你的 Notion 页面。基于这些结果,你可以继续深入细节,或开始事实核查。
9.3 AI 代理的框架(Framework for AI agents)
与其让用户直接与 LLM 对话,许多框架会提供抽象层并增加功能来简化交互,例如提示增强与检索增强生成(RAG) 。如果用户为 AI 代理授予了权限,代理就可以访问特定资源(如个人数据)。
我们可以对照本章开头的图 9.1来考察 AI 代理的功能。开发者对无代码与编码两种路径在计算机科学各领域的分野并不陌生。数据集成就是一例:有的团队通过编写代码与 Apache Kafka 等框架交互,把数据推入 Kafka 主题,再由订阅该主题的所有消费者分发;而 Apache NiFi 等其他框架则提供图形界面(UI) ——用户点选数据源、拖到目的地并配置细节。
公司常常重视能降低复杂度的工具,因为他们缺少足够的编程人才。但批评者认为“所见即所得”的工具也只是逻辑的另一层抽象,且常常缺少核心功能。也许一些团队能很快“点点点”做出首个 Demo,但当工程师要把它上生产时,集成那些非定制能力反而会更花时间,仿佛还不如程序员自己从零实现。
在使用 AI 代理时,开发者同样会遇到类似的战略抉择。有些框架允许工程师通过 UI 创建代理,其中一个叫 n8n。要用 n8n 构建工作流,你需要在平台上注册账户以进入其工作流 UI。用户可以在画布上创建工作流,把它连接到一个输入(如即时通讯或聊天窗口),并把各种服务纳入流程,比如语音转文本、或基于输入向 LLM 提交提示。此类拖拽工具通常看视频比看书更易上手,网上也有大量交互式教程手把手带你搭建工作流。本文将把重点放在用编程逻辑构建 AI 代理,这往往更适合在书里讲清楚。
9.3.1 从一次性提示到代理(From one-shot prompting to agents)
很多视频会教你点哪里、如何把上一步的结果拿来继续处理。鉴于本书名为“程序员的投资学”,我们侧重用开发工具创建通用工作流。LangChain 是一个用于开发基于 LLM 的应用的框架,它简化了 LLM 应用的全生命周期,并集成了广泛且不断增长的提供商列表(mng.bz/xZx6)。
提供商(provider)负责把数据源与 LLM 连接起来。清单 9.4 展示了一个示例:利用 LangChain 的模块封装对 OpenAI LLM 的访问,执行一次性提示。
清单 9.4 使用 LangChain 执行一次性提示(one-shot prompt)
from langchain_openai import ChatOpenAI
OPENAI_KEY = os.getenv("ai.openai.pwd")
llm = ChatOpenAI(
model="gpt-4o",
temperature=0,
max_tokens=None,
timeout=None,
max_retries=2,
api_key=OPENAI_KEY)
messages = [
(
"system",
investment_persona,
),
("human", "Provide a SWOT analysis of Google."),
]
ai_msg = llm.invoke(messages) #1
ai_msg
#1 Submits the prompt
第 8 章我们已用 OpenAI 原生库实现过相同功能。现在可以继续集成其他 LLM,如 Anthropic(mng.bz/AGXQ),会发现代码结构大同小异。
NOTE 第 8 章中我们解释了如何创建投资者画像。每当你用 LLM 分析投资机会,都要尽量把关于你自己的信息充分提供给模型。
Python 是以约定著称的语言:例如对集合,不论底层数据结构如何,len() 都返回元素数量。借助 LangChain,我们也得到类似的统一性:不管底层是哪家 LLM,invoke() 方法都表示“把提示发送给 LLM”。这让我们更容易接入新模型;若用各家的原生 SDK,就得逐一学习细节。这是个良好开端,但还不是 LangChain 的主要价值。
继续看清单 9.5,这段代码把 Yahoo Finance News 集成进对话。
清单 9.5 在 LangChain 中使用 Yahoo
from langchain.agents import AgentType, initialize_agent
from langchain_community.tools.yahoo_finance_news import YahooFinanceNewsTool
tools = [YahooFinanceNewsTool()]
agent_chain = initialize_agent(
tools,
llm, # see listing 9.4
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
)
agent_chain.invoke(
"What happened today with Microsoft stocks?", #1
)
#1 Submits the agent
该片段凸显了与直接调用 LLM的本质差异:我们使用代理(agent)把数据源接到 LLM 上。在示例中,我们把 verbose 设为 True。来看看代理的输出。下面是清单 9.5 在 2025 年 4 月执行时的节选回放:
Entering new AgentExecutor chain . . .
I should check the latest financial news for Microsoft to see what happened today.
Action: yahoo_finance_news
Action Input: MSFT
Observation: Is Microsoft (MSFT) the Best Machine Learning Stock to Buy Now?
...
Thought: Based on the news articles, it seems like Microsoft is still a strong player in the machine learning and AI markets.
Final Answer: Microsoft stocks are still performing well in the machine learning and AI sectors.
Finished chain.
如上所示,我们让 LangChain 构建了一个会去 Yahoo Finance 抓新闻并将其并入研究的 AI 代理。在运行研究时,我们还可以串接更多工具。LangChain 也允许我们把自有数据以自定义提供商的方式纳入代理。这使得 LangChain 成为一个有助于研究的强大框架。
9.3.2 检索增强生成(Retrieval-augmented generation)
通过为 LLM 提供训练阶段不可达的额外数据,可以增强其生成性能。典型候选包括:近期数据(如模型训练后才产生的数据)或受限数据(如企业内部数据)——它们都适合做 RAG。
前面的例子展示了如何用 Yahoo Finance 集成近期数据。从新闻源拉取信息可以弥补模型训练后的缺口。然而,理论上可集成的数据源不计其数。我们会在第 11 章概述若干来源,规模之大以至于你不愿在提示时临时从公共 Web 抓取。我们需要一种结构化的方式来提供这些额外数据的访问权限,包括把它们存入数据库。
在下面的示例中,我们将把公司财报电话会(earnings call)实录加入到提示。若某个 LLM 训练于数月之前,它就无法访问这些近期活动的数据。如何获得财报会实录?各类金融分析平台会提供实录文本,但问题是能否爬取。
当数据量很大时,你需要把它们放进既安全又可访问的容器中。为此我们分工处理:一个进程从多源抓数据并加载入数据库;提交提示是另一条路径,在这条路径上 LLM 把数据库当做扩展大脑来用。下面开始动手。
加载数据(LOADING DATA)
我们需要从不同来源抓数据:使用爬虫从多个金融分析平台抓取网页、从下载文档中提取信息、或通过 API/数据库加载。有些数据也能通过公共数据交换获得。作为参考,你可以从 AWS 的数据市场加载 EDGAR(美国证监会电子申报系统)中的公司文件(mng.bz/Z9a9)。
专业对冲基金与投研机构会把来自多个来源(包括许多商业提供商)的数据汇入数据平台——这超出了本书的范围。对一本书而言,从多个网站抓取数据也有风险:平台常改内部结构,示例代码可能很快就失效。若你对爬取感兴趣,可以研究 Beautiful Soup 等框架与其他多源加载库。
因此,书中提供了若干汽车行业公司的财报会实录文本文件供下载。你也可以自行添加数据来实验代码。
清单 9.6 展示了如何把这些文本文件加载进内存。加载逻辑对大多数程序员都不陌生;需要注意的是,我们把加载的文本加入 LangChain 专用的数据结构,以便在提交提示时作为附加文档使用。
清单 9.6 将文本文件加载到内存(Loading text files into memory)
from langchain_core.documents.base import Document
all_docs = []
files = ["pony.txt", "oust.txt", "lazr.txt", "invz.txt", "aeva.txt"]
for f in files:
file = open(f, "r")
content = file.read()
doc = Document(content) #1
all_docs.append(doc)
file.close()
#1 Loads data into specific containers for LangChain
接着补充上下文。在这个参考示例中,我们想探究自动驾驶公司。我们会设置两种不同的上下文来验证功能:其一,我们表明看好激光雷达(LiDAR) ;其二,我们表明偏好不依赖 LiDAR 的公司。稍后会分别提交这两种上下文验证答案是否有差异。我们预期根据偏好(是否倾向 LiDAR 技术)会得到不同推荐。
清单 9.7 展示了如何定义并把这些偏好加入目录。注意:如果要用不同上下文重新跑示例,需要重建数据结构。
清单 9.7 提供额外上下文(Providing additional context)
generic = """I have a computer science background. I have worked on various
digital transformation projects for multiple companies. I understand well
how AI can change companies' businesses. I am looking for stocks of
companies that might get transformed through AI but have not yet been
overrun by investors."""
pro_lidar_text = """I believe in the future of autonomous driving.
Autonomous driving needs a LiDAR system. Do not recommend any companies
that reject LiDAR."""
con_lidar_context = """Please prefer companies that focus on solutions
without LiDAR systems as they are expensive."""
all_docs.append(Document(generic + pro_lidar_text))
执行完上述脚本后,所有文档已汇集。接下来需要把这些数据转换为可提交给 LLM 的格式。由于内存中的文本体量较大,我们需要把它切分/向量化为 LLM 更容易理解与检索的形式。
准备数据(PREPARING DATA)
我们已经把数据加载到内存中;要在 RAG 中使用全部内容,需要将其加入向量数据库。向量数据库是一种用于存储向量嵌入(embeddings)的容器。现在,内存里的所有可变内容都必须映射为向量。这有助于我们编排工作流:在提交提示词时,能够对这些附加信息进行评估与处理。在更高级的场景中,我们会累积海量信息,并将其存入专用数据库(如 PostgreSQL 或 MongoDB)。为了将注意力集中在用例本身、不被数据平台管理分散精力,本参考示例使用内存型数据库。在清单 9.8 中,我们把文档数据拆分为子文档。
清单 9.8 数据分块(Chunking data)
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
add_start_index=True,
)
all_splits = text_splitter.split_documents(all_docs)
print(f"Split blog post into {len(all_splits)} sub-documents.")
当我们把文档拆分为足够多的 chunk 后,需要将它们转换为嵌入。各家 LLM 的做法有所不同。清单 9.9 展示了如何使用 OpenAI embeddings 把所有 chunk 加入内存向量数据库。
清单 9.9 将全部文本加入内存型向量数据库
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
from langchain_core.vectorstores import InMemoryVectorStore
vector_store = InMemoryVectorStore(embeddings)
document_ids = vector_store.add_documents(documents=all_splits)
准备就绪后,我们进入最后阶段:提交提示并评估结果。我们预期:当我们声明“相信 LiDAR”或“不偏好 LiDAR”时,会得到不同的结果;同时,回答中也会反映来自 RAG 的部分内容。
调用(CALL)
在 RAG 中,我们会构建执行图(execution graphs) 。这涉及用于编排工作流的数据结构。用过 Apache Spark 或 Apache Airflow 的程序员对有向无环图(DAG)并不陌生。我们创建一个名为 Stage 的类(注:下文代码里叫 State),其中包含问题、上下文与回答等变量。
我们还为两个阶段(stage)定义了方法:其一从向量数据库检索数据。显然,需要从内存里取哪些内容取决于作为参数传入的问题。最后,我们需要提交提示。清单 9.10 展示了这部分逻辑:retrieve 与 generate 两个方法代表处理提示的两个独立步骤。
清单 9.10 创建辅助结构(Creating helper structures)
from langchain_core.documents import Document
from typing_extensions import List, TypedDict
from langchain import hub
prompt = hub.pull("rlm/rag-prompt")
class State(TypedDict):
question: str
context: List[Document]
answer: str
def retrieve(state: State):
retrieved_docs = vector_store.similarity_search(state["question"])
return {"context": retrieved_docs}
def generate(state: State):
docs_content = "\n\n".join(doc.page_content for doc in state["context"])
messages = prompt.invoke({"question": state["question"], "context": docs_content})
response = llm.invoke(messages)
return {"answer": response.content}
下一步,我们定义问题并初始化 LLM。此处示例使用 OpenAI 的 GPT-4.1 模型:
from langchain.chat_models import init_chat_model
llm = init_chat_model("gpt-4.1", model_provider="openai")
question = "Recommend companies in the autonomous driving sector with good"
" recent earnings."
我们的询问聚焦自动驾驶。为保持清晰,我们让提示尽量短——在实际场景下,你往往会像第 8 章那样提供更多上下文与细节。
清单 9.11 是执行前的最后一步,展示了编排如何工作。我们把两个步骤加入一个状态机,并将检索设为起点。另一种顺序(先生成、后检索)显然不合逻辑。请记住,执行工作流会随时间变得更复杂。最后一行用于显示执行图。
清单 9.11 构建执行图(Building an execution graph)
from langgraph.graph import START, StateGraph
from IPython.display import Image, display
graph_builder = (StateGraph(State).add_sequence
([retrieve, generate])) #1
graph_builder.add_edge(START, "retrieve") #1
graph = graph_builder.compile()
display(Image(graph.get_graph().draw_mermaid_png())) #2
#1 Adds edges to the graph
#2 Displays the execution graph
图 9.4 概述了执行图:我们先检索、再生成。在更复杂的场景中,执行可能更为繁复;但对本用例而言,两阶段已足够。
图 9.4 RAG 引擎的执行图:先从 RAG 检索信息,再结合 RAG 与 LLM 数据生成输出
最后,调用整个图,把问题作为提示提交;调用过程中也会从向量数据库检索信息:
result = graph.invoke({"question": question})
print(f'Answer: {result["answer"]}')
当作者在偏向 LiDAR 的正向上下文下运行时,得到如下回答:
Answer: 基于近期强劲的业绩表现以及在自动驾驶 LiDAR 技术方面的领先地位,推荐 Innoviz。Innoviz 已与 Mobileye 等关键参与者建立合作,并与 NVIDIA 平台集成,同时扩大量产以满足不断上升的需求。此外,使用 LiDAR 技术并获得监管批准、开展商业车队运营的中国机器人出租车公司也处于有利位置,尽管在所提供的上下文中未给出具体公司名称。
我们得到了对 Innoviz 的推荐;如果回看其财报电话会的实录,也会发现与 Mobileye 合作的相关表述。
接下来考察反向场景:我们声明不偏好 LiDAR。虽然最近的 Mobileye 财报电话会不在提供的文档中,但从互联网上可以查到 Mobileye 已经暂停其 LiDAR 相关投入:
Answer(摘要): 结合上下文,Mobileye 是自动驾驶领域中值得关注的公司,近期合作、整车项目与业绩动能强劲,聚焦不重度依赖昂贵 LiDAR 的 AI 解决方案。上下文还提到多家机器人出租车公司在监管推进与商业化规模上的显著进展,显示该赛道头部玩家的基本面稳健。考虑到你倾向规避以 LiDAR 为中心的路线、并需要尚未被投资者“挤爆”的公司,Mobileye 脱颖而出,是颇具潜力的候选者。
现在,我们可以在接入多种数据源的前提下运行代理。到此阶段的工作先告一段落;接下来可以转向图表与技术分析。本章中的许多细节都基于前文的观点:投资于你所了解的。同理,想获得好的答案,就要知道该问什么——这会引出“我们需要再提供哪些额外上下文”的问题。最后一步,就是把问题作为提示提交,并对回答进行评估。
总结(Summary)
- 人与 AI 的交流有相通之处。借助合适的工具可以提升你的沟通能力。
- 为了更高效地沟通,我们可以获取原本不可及的特定信息来源,从而在交流时制定更有效的策略。
- 我们可以把提示词存入一个简单的数据库,以便后续复用。
- 如果要研究专项主题,可以把所有提示词集中存入数据库并打上标签,以便按需检索。
- Notion 是一款笔记工具,能把你所有的研究都导出到一个地方集中管理。
- 在没有专门框架来辅助 AI 代理的情况下,直接与 LLM 协作通常需要大量自定义工作。例如,为 RAG 连接特定数据源,就需要编写相当多的代码把数据接入 LLM。
- AI 代理有助于将与 AI 的对话自动化、流程化。
- AI 代理通常内置了面向附加数据源的提供商(providers) ,让我们可以编排 LLM 来获取新的洞见。
- n8n 是一款无代码框架,可用 AI 代理来编排工作流。
- LangChain 是一个用于开发 LLM 驱动应用的框架,可将不同数据源集成进 AI 代理的工作流。
- LangChain 简化了对 LLM 的访问,使所有触发提示的调用在语法上趋于一致。
- LangChain 拥有广泛的内置提供商,并为程序员提供接口以添加自定义提供商。
- 借助 LangChain,我们可以创建 AI 代理:先查询最新财经新闻,再在 LLM 中调用提示,并把检索到的信息并入分析。
- LangChain 提供日志与追踪机制,可记录 AI 代理在执行过程中的工作流,帮助我们洞察其运行方式。
- RAG 是一种让 LLM 在其训练数据之外获取额外数据的思路。
- 为了用于生成回答,我们可以把补充信息存放在向量数据库中;为此需要将数据转换为特定的数据结构,即向量嵌入(embeddings) 。
- 在许多复杂用例中,我们需要专门的数据平台(如 RAG 数据库);而在简单用例中,内存型向量数据库就足够了。
- 我们可以把执行工作流建模为图形(graph) ,以此来编排整个流程。