返回

Llama 3解析Markdown失败?用Python搞定版本记录提取

Ai

搞定 Markdown 解析:Llama 3 不给力?试试这些招儿

你是不是也遇到了这样的情况:想用 Llama 3 这种强大的语言模型来帮你解析 Markdown 文件,比如从一堆版本迁移记录里提取信息,再整理成一个漂亮的表格。想法很美好,写了个 Python 脚本,用了 Ollama 库,还特意定制了个 Llama 3 模型,把上下文窗口 (num_ctx) 拉到 32768,觉得万无一失了。

结果呢?处理小猫两三条记录还行,虽然慢得像蜗牛爬(等了快 50 分钟),但好歹有结果。可一旦把整个 migrations.md 文件扔进去,Llama 3 就开始“胡言乱语”,给了一段不知所云的 JSON 片段,还反问你“想用这些数据干嘛?” 这离我们想要的那个整齐的 Markdown 表格,差了十万八千里。

# 这是你的 Python 代码,尝试用 Llama 3 解析 Markdown
import ollama
import re

prompt = re.sub(r"\s+", " ", f"""
# ... (此处省略了你超长、超复杂的提示词) ...
Please sort the entire output table based on the values in the From PWA version column.
Response only with the result table, please.
Here is the input data:
""".strip())

with open('migrations.md') as f:
    migrations_description = f.read()
    prompt = prompt + migrations_description

def main(prompt_text):
    print("Please wait ...")
    response = ollama.chat(model='intershop-pwa-changelog-converter',
                           messages=[{'role': 'user', 'content': prompt_text}])
    with open('response.md', 'w') as file:
        file.write(response['message']['content'])
    print("All done!")

if __name__ == '__main__':
    main(prompt)

还有你的 Modelfile:

FROM llama3

PARAMETER temperature 1
PARAMETER num_ctx 32768

碰上这种事儿,确实挺让人头疼的。别急,咱们来分析分析,看看问题到底出在哪儿,再找找靠谱的解决办法。

一、为啥 Llama 3 会“翻车”?

直接把这么复杂的任务甩给 Llama 3,效果不理想,其实不完全是 Llama 3 的锅。这里面有几个原因:

  1. 任务太复杂,LLM 扛不住:
    看看你的提示词(Prompt),里面塞了多少活儿?

    • 根据 ## From [版本号] to [版本号] 模式 查找和提取 内容。
    • 对提取的内容进行 文本处理 (替换时态)。
    • 在内容里 查找特定信息 (Angular/Node/NPM 版本),找不到还得用 上一行 的或者 默认值 。这涉及到跨记录的上下文依赖和条件逻辑。
    • 最后,把所有信息整合成一个 特定格式的 Markdown 表格 ,还得包含 硬编码的值
    • 完了还得 排序

    这么多步骤,每一步都有可能出错。语言模型处理这种高度结构化、多步骤、带条件逻辑的任务,本身就不是强项。它更擅长理解和生成自然语言,而不是像程序一样精确执行一长串指令。你等于是在让一个诗人去精确计算火箭轨道,有点强人所难了。

  2. 上下文窗口 (Context Window) 的“陷阱”:
    你把 num_ctx 设置到了 32768,看起来很大。但对于模型来说,在这么长的文本里,同时记住并精确执行你那一长串复杂的指令,还要处理那么多的 Markdown 细节,是非常困难的。模型的“注意力”是有限的,处理长序列和复杂指令时,很容易“忘记”前面的要求,或者搞混不同步骤的逻辑。它可能抓住了你输入数据的主要内容(Angular 2 迁移),但无法完美执行所有细节操作。那段奇怪的 JSON 输出,就是它“尽力了”但没成功的证明。

  3. 提示工程 (Prompt Engineering) 的挑战:
    你写的 Prompt 非常详细,意图是好的。但是,对于 LLM 来说,这么长的、包含大量特殊指令(比如 [resulted content] 这种占位符)的 Prompt,可能反而会增加理解难度。模型可能会误解某些指令,或者无法将所有指令正确地关联起来。而且,temperature 设置为 1,鼓励了模型的“创造性”,这对于需要精确输出的任务来说,可能不是好事,更容易产生意想不到的结果。

  4. LLM 的不确定性:
    语言模型本质上是概率性的。即使是同样的输入,每次运行的结果也可能不完全一样(尤其当 temperature 较高时)。对于需要稳定、精确结果的数据处理任务,这种不确定性是个大问题。而你的需求,恰恰是需要高度精确和格式化的输出。

简单说,你让 Llama 3 干了一件更适合用传统编程方法来解决的活儿。

二、换个思路:靠谱的解决方案

既然直接让 Llama 3 包揽全局不太行,咱们就得换个策略。核心思想是:让专业的工具干专业的事 。代码擅长精确的模式匹配、逻辑判断和数据处理,就让代码来;LLM 擅长理解和处理自然语言的细微之处,那就把它用在刀刃上(如果真的需要的话)。

方案一:代码为主,LLM 为辅(混合动力法)

这种方法是,主要的解析、提取、结构化工作交给 Python 代码,只在某些代码不好处理的“软”任务上(比如复杂的语义理解或文本改写)才调用 LLM。

原理和作用:

利用 Python 强大的字符串处理和正则表达式能力,先完成基础的结构化解析。比如,把 Markdown 文件按 ## From ... to ... 切分成独立的记录块,提取各个版本号。对于时态替换这种任务,如果规则比较固定(比如简单过去时 -> 将来时),正则表达式也能搞定。如果涉及到更复杂的自然语言理解,可以考虑把这部分 单独 交给 LLM 处理。

操作步骤:

  1. Python 读取文件: 这个你已经做了。

  2. 切分记录: 使用正则表达式找到所有 ## From ... to ... 的标题,把 Markdown 文件切分成多个部分。每个部分对应一个迁移版本。

    import re
    
    with open('migrations.md', 'r', encoding='utf-8') as f:
        content = f.read()
    
    # 使用正则表达式按 "## From ..." 分割文本
    # 注意:(?=...) 是正向前瞻,确保分割点本身包含在下一段的开头
    # re.DOTALL 让 '.' 可以匹配换行符
    sections = re.split(r'(?=## From \d+(\.\d+)* to \d+(\.\d+)*)', content, flags=re.DOTALL)
    
    migration_blocks = []
    for section in sections:
        if section.strip().startswith("## From"):
             migration_blocks.append(section.strip())
    
    # print(f"找到了 {len(migration_blocks)} 个迁移记录块")
    
  3. 提取核心信息(Python + Regex): 遍历每个 migration_block,用正则表达式提取 "From version", "To version",以及记录内容。

    extracted_data = []
    for block in migration_blocks:
        from_to_match = re.match(r'## From (\S+) to (\S+)', block)
        if from_to_match:
            from_version = from_to_match.group(1)
            to_version = from_to_match.group(2)
            # 记录内容是标题行之后的所有内容
            content_start_index = from_to_match.end()
            migration_content = block[content_start_index:].strip()
    
            extracted_data.append({
                'from': from_version,
                'to': to_version,
                'raw_content': migration_content,
                # 先把其他字段置空,后面再填充
                'angular': None,
                'nodejs': None,
                'npm': None,
                'processed_content': None 
            })
    # print(extracted_data[0] if extracted_data else "没有提取到数据")
    
  4. 提取依赖版本(Python + Regex + 逻辑): 这一步稍微复杂点,需要处理“找不到就用上一行或默认值”的逻辑。

    default_angular = "11"
    default_nodejs = "14.15.0 LTS"
    default_npm = "6.14.8"
    previous_angular = default_angular
    previous_nodejs = default_nodejs
    previous_npm = default_npm
    
    for item in extracted_data:
        content = item['raw_content']
    
        # 尝试用正则查找版本
        angular_match = re.search(r'Angular(?: version)?\s*(\d+(\.\d+)*)', content, re.IGNORECASE)
        nodejs_match = re.search(r'Node\.?js(?: version)?\s*([\d\.]+ LTS?)', content, re.IGNORECASE)
        npm_match = re.search(r'NPM(?: version)?\s*(\d+(\.\d+)*)', content, re.IGNORECASE)
    
        # 找不到就用上一条记录的,或者默认值
        item['angular'] = angular_match.group(1) if angular_match else previous_angular
        item['nodejs'] = nodejs_match.group(1) if nodejs_match else previous_nodejs
        item['npm'] = npm_match.group(1) if npm_match else previous_npm
    
        # 更新 "previous" 值,供下一轮循环使用
        previous_angular = item['angular']
        previous_nodejs = item['nodejs']
        previous_npm = item['npm']
    
    # print(extracted_data[0] if extracted_data else "没有提取到数据")
    
  5. 处理文本内容(可选 LLM 或 Regex): 这一步是替换时态。

    • Regex 方案(推荐): 如果时态替换规则明确,用 re.sub() 通常更稳定、更快。你需要定义一些匹配过去时态动词的模式,并替换成将来时态。这可能需要一些语言学知识或者多试几次正则模式。例如,简单的 "was" -> "will be", "updated" -> "will update"。这可能不完美,但对于技术文档通常够用。
      def simple_tense_change(text):
          # 这是一个非常简化的示例,实际需要更复杂的规则
          text = re.sub(r'\bwas\b', 'will be', text)
          text = re.sub(r'\b(updated|migrated|removed|added)\b', r'will \1', text)
          # ... 可能需要更多规则
          return text
      
      for item in extracted_data:
          item['processed_content'] = simple_tense_change(item['raw_content'])
      
    • LLM 方案(备选): 如果你觉得 Regex 搞不定,或者文本非常口语化、时态复杂,可以考虑对每个 item['raw_content'] 单独 调用 Llama 3,只给它这一个任务。但要小心: 这会让处理速度大大减慢,并且仍然有 LLM 不按要求输出的风险。你需要设计一个非常专注的 Prompt,比如:
      # (这是调用 LLM 的伪代码,需要用 ollama 库实现)
      # ollama_prompt = f"Please rewrite the following text by changing past simple and present perfect tenses to future simple tense. Respond only with the rewritten text:\n\n{item['raw_content']}"
      # response = call_ollama(ollama_prompt) # 你需要实现这个调用函数
      # item['processed_content'] = response
      
      安全建议: 如果调用外部 LLM API 而不是本地 Ollama,注意 API 密钥管理和成本。
  6. 格式化输出与排序(Python): 现在所有数据都在 extracted_data 这个列表里了,处理起来就很容易了。

    from packaging.version import parse as parse_version # 用于版本号排序
    
    # 排序: 需要处理版本号字符串,建议用库来比较
    # key 函数尝试解析版本号,失败则返回一个很小的版本对象以排在前面
    def get_sort_key(item):
        try:
            # 处理可能存在的非数字版本,或者附加字符
            version_str = re.match(r'(\d+(\.\d+)*)', item['from']).group(1)
            return parse_version(version_str)
        except:
            # 如果解析失败(例如版本号格式奇怪),给一个极小值
            return parse_version("0.0.0") 
    
    extracted_data.sort(key=get_sort_key)
    
    # 生成 Markdown 表格
    markdown_table = "| From PWA version | To PWA version | Angular version | NodeJS version | NPM version | Things we need to do | Estimate | Regression |\n"
    markdown_table += "|---|---|---|---|---|---|---|---|\n"
    
    for item in extracted_data:
        # Markdown 表格内容里的换行符和 | 需要处理
        content_escaped = item['processed_content'].replace('|', '\\|').replace('\n', '<br/>')
        markdown_table += f"| {item['from']} | {item['to']} | {item['angular']} | {item['nodejs']} | {item['npm']} | {content_escaped} | 1 | two days |\n"
    
    # print(markdown_table)
    with open('response_python.md', 'w', encoding='utf-8') as f:
        f.write(markdown_table)
    
    print("All done using Python script!")
    

进阶使用技巧:

  • 可以考虑使用更健壮的 Markdown 解析库(如 mistunemarkdown-it-py)来辅助解析 Markdown 结构,而不是完全依赖正则表达式,特别是在 Markdown 结构更复杂的情况下。
  • 版本号比较使用 packaging.version 库更可靠,能处理各种复杂的版本号格式。

优点:

  • 大部分处理由代码完成,结果稳定、可预测。
  • 速度快得多,尤其是纯代码方案。
  • 更容易调试和维护,因为逻辑都在代码里。

缺点:

  • 需要编写和调试更多的 Python 代码。
  • 如果真的需要 LLM 做某些复杂的文本处理,仍然需要调用 LLM,并处理其不确定性。

方案二:纯代码方案(硬核解析法)

这是方案一的彻底版本,完全放弃 LLM,所有工作都用 Python 和正则表达式完成。对于你的这个任务,这很可能是最合适、最高效的方法

原理和作用:

利用 Python 的内置功能和强大的 re 模块,实现对 Markdown 文本的完全控制解析和转换。所有规则和逻辑都显式地写在代码里。

操作步骤:

基本同方案一的步骤 1 到 4,加上步骤 5 的 Regex 方案 ,以及步骤 6。完全不需要 ollama 库和任何 LLM 调用。

代码示例:

参考方案一中各个步骤的 Python 代码片段,将它们整合到一个完整的脚本里。关键在于时态替换也用 re.sub() 实现 ,就像方案一里展示的那样。

优点:

  • 最快、最稳定: 没有 LLM 调用的延迟和不确定性。
  • 完全可控: 所有逻辑都在你的代码里,方便修改和测试。
  • 资源消耗低: 不需要运行 Llama 3 模型。
  • 零成本: 不需要支付 LLM API 费用(如果用的是云服务)。

缺点:

  • 编写 Regex 可能费劲: 特别是时态替换的规则,可能需要反复调试才能覆盖大部分情况。
  • 不适用于模糊或语义任务: 如果需求变为“总结迁移要点”或“判断迁移风险等级”,纯代码方案就比较难了,那时才需要考虑 LLM。

方案三:优化 LLM 提示(风险仍在,不太推荐)

如果你非要坚持用 LLM,可以尝试优化提示词和使用方法,但成功率可能依然不高,特别是对于大文件。

原理和作用:

尝试通过改进与 LLM 的交互方式,引导它更好地理解和执行任务。

操作步骤:

  1. 分解任务 (Prompt Chaining): 不要在一个 Prompt 里让 LLM 干所有事。可以试试:

    • 第一步:只让 LLM 提取 ## From ... to ... 块和基本版本号。
    • 第二步:对每个提取块的内容,再发一个 Prompt 让 LLM 做时态转换。
    • 第三步:再发一个 Prompt 让它提取依赖版本(处理上一行逻辑会非常难)。
    • 第四步:最后整合信息生成表格。
      缺点: 非常慢,多次调用增加了出错概率和成本。
  2. 少样本学习 (Few-Shot Learning): 在你的 Prompt 里,开头给出 1-2 个完整的例子。包括输入 Markdown 片段和对应的完整输出 Markdown 表格行。这能给模型一个更具体的模仿样本。

    Here are some examples:
    
    Input section:
    ## From 0.25 to 0.26
    - We updated Angular to version 10.
    - NodeJS was 12.x.
    
    Output table row:
    | 0.25 | 0.26 | 10 | 12.x | 6.14.8 | - We will update Angular to version 10.<br/>- NodeJS will be 12.x. | 1 | two days |
    
    --- 
    Now, process the following input data based on the rules and examples:
    [你的 migrations.md 内容]
    
  3. 简化指令和输出格式:

    • 尝试让 LLM 先输出 JSON 格式,每条记录一个 JSON 对象。这样结构化程度更高,方便你用代码后续处理成 Markdown 表格。但从你的错误输出来看,它好像尝试过但失败了。
    • 减少指令数量,比如去掉时态转换,或者去掉复杂的版本依赖逻辑,看能否让核心提取任务成功。
  4. 调整模型参数:

    • 降低 temperature,比如设置为 0.50.7,减少随机性,让输出更贴近指令。
    • 检查 Ollama 是否还有其他可以调整的参数,比如限制输出 token 数量(虽然这里你希望它完整输出)。

进阶使用技巧:

  • 某些高级 LLM 平台支持“函数调用 (Function Calling)”或“工具使用 (Tool Use)”,可以让 LLM 调用你定义的代码函数来执行特定任务(如 Regex 匹配)。如果 Ollama 或其支持的模型将来支持类似功能,会是混合方案的更优实现。

安全建议:

  • 同方案一,注意 API 密钥和成本(如果不使用本地 Ollama)。
  • 小心 LLM “幻觉”,它可能会编造信息或错误地执行指令,结果需要仔细检查。

但总的来说,对于这种规则明确、结构化的数据处理任务,强烈建议优先考虑方案一(混合)或方案二(纯代码),特别是方案二。


别灰心,Llama 3 是个强大的工具,但得用对地方。对于你目前这个 Markdown 解析和表格生成的任务,用 Python 代码来主导,甚至完全由代码搞定,会是更稳妥、更高效的选择。把 LLM 留给那些真正需要自然语言理解能力的硬骨头吧!